Skip to content

Resource options: implement immutable for Project/Domain/Role, close User enforcement gaps #1096

Description

@gtema

Context

Evaluated how "resource options" are implemented here versus Python Keystone
(keystone/common/resource_options/ + per-resource registries).

Correction to initial framing of this issue: Python's resource_options
framework is not User-only. It's also registered for Project/Domain (which
share one backend/table upstream, keyed by is_domain) and for Role, in both
cases with a single option: immutable (option_id 0000, boolean). It's
enforced in the update/delete paths — a resource with options.immutable == true rejects update/delete with ResourceUpdateForbidden /
equivalent. Purpose: let bootstrap-critical resources (default domain, the
bootstrap admin project, admin/member/reader roles) be protected from
accidental modification or deletion via the API. This is a real compatibility
gap, not a speculative addition — striking my earlier "don't add one
speculatively" recommendation for these three resources.

User is already at good parity. UserOptions
(crates/core-types/src/identity/user.rs:230-249) reproduces Python's
USER_OPTIONS_REGISTRY field-for-field, including the numeric option IDs
(1000-1004, MFAR, MFAE) in
crates/identity-driver-sql/src/user_option.rs:26-59 and
crates/config/src/identity.rs:88-100. Global defaults live in
SecurityComplianceProvider (crates/config/src/security_compliance.rs),
with per-user options overriding them — the same two-layer pattern Python
uses. This override pattern is already correctly wired for lockout
(authenticate.rs:96), password expiry (authenticate.rs:139), and
inactivity (user.rs:89, service_account/get.rs:65).

Gaps found

1. immutable missing for Project/Domain/Role (the main gap) — implemented, see below

The DB schema is already scaffolded and forgotten:

  • crates/resource-driver-sql/src/entity/project_option.rsproject_option
    table (project_id, option_id, option_value), table creation wired at
    crates/resource-driver-sql/src/lib.rs:299.
  • crates/role-driver-sql/src/entity/role_option.rsrole_option table,
    same shape, table creation wired at crates/role-driver-sql/src/lib.rs:327.
  • No separate domain_option table exists, and none is needed: Domain rows
    live in the project table (is_domain: bool on
    crates/resource-driver-sql/src/entity/project.rs:32), mirroring Python's
    unified project/domain backend — so project_option already covers both.

Operator-configurability question this raises: immutable is not a
global config toggle in Python — it's a per-resource flag, set at creation
(bootstrap sets it on the default domain/admin project/base roles) or via the
resource's own update API afterward by an admin who wants to protect a
specific resource.

2. Stored-but-unenforced UserOptions fields — still open

Two of the eight User option fields are round-tripped through the API/DB but
never read by any auth or write-path code:

  • lock_password — should prevent the user themself from changing their own
    password (admin/domain-admin reset still allowed), per Python's
    LOCK_PASSWORD_OPT semantics.
  • ignore_change_password_upon_first_use — should be the per-user escape
    hatch from [security_compliance] change_password_upon_first_use,
    mirroring ignore_password_expiry / ignore_lockout_failure_attempts.

(multi_factor_auth_rules / multi_factor_auth_enabled enforcement is
already tracked separately in #264 and #266, not in scope here.)

3. Dead code — still open

crates/keystone/src/api/v3/user/types.rs:23-70 is a fully commented-out
duplicate of the UserOptions/UserResponse conversions that now live in
crates/api-types/src/v3/user_conv.rs. Should be deleted.

4. Stray TODO — resolved

crates/core-types/src/resource/project.rs:112's // TODO: add options is
removed; the field now exists (see below).

Acceptance criteria

  • ProjectOptions/RoleOptions added to core-types, each exposing
    immutable: Option<bool>, following the UserOptions shape. Domain
    reuses ProjectOptions (it shares the underlying project row and
    project_option table).
  • Persistence wired through the existing project_option/role_option
    tables, mirroring crates/identity-driver-sql/src/user_option.rs's
    to_model_iter/FromIterator pattern (upsert via ON CONFLICT, bulk
    load_many merge on list).
  • options exposed on Project/Domain/Role create/update/response API
    types.
  • Update and delete paths for project, domain, and role reject the
    operation when options.immutable == true unless the same request
    clears it (ResourceProviderError::Immutable /
    RoleProviderError::Immutable → 403 Forbidden), implemented in
    ResourceService/RoleService.
  • Verify against current bootstrap logic whether the default domain,
    bootstrap admin project, and base roles (admin/member/reader)
    should be created with immutable: true for parity. Deliberately not
    implemented
    in this pass — keystone-manage bootstrap here talks to
    the API as a plain client and is idempotent/re-runnable (e.g. to fix up
    an existing install); auto-marking its output immutable would be a
    behavior change with its own test/operational surface, not something
    "implement the immutable option" implies on its own. Left as a follow-up
    decision.
  • lock_password blocks self-service password change; admin/domain-admin
    reset path unaffected. Tests for both.
  • ignore_change_password_upon_first_use correctly bypasses
    [security_compliance] change_password_upon_first_use the same way
    ignore_password_expiry bypasses password_expires_days.
  • Dead commented-out code in crates/keystone/src/api/v3/user/types.rs
    removed.
  • project.rs:112 TODO resolved.

Implemented on claude/resource-options-support-qn3ilz
(688d3ce).

References

Metadata

Metadata

Assignees

Labels

Type

Projects

Status
Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions