You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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/role-driver-sql/src/entity/role_option.rs — role_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.
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).
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_optionsframework 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 bothcases with a single option:
immutable(option_id0000, boolean). It'senforced in the update/delete paths — a resource with
options.immutable == truerejects update/delete withResourceUpdateForbidden/equivalent. Purpose: let bootstrap-critical resources (default domain, the
bootstrap admin project,
admin/member/readerroles) be protected fromaccidental 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'sUSER_OPTIONS_REGISTRYfield-for-field, including the numeric option IDs(
1000-1004,MFAR,MFAE) incrates/identity-driver-sql/src/user_option.rs:26-59andcrates/config/src/identity.rs:88-100. Global defaults live inSecurityComplianceProvider(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), andinactivity (
user.rs:89,service_account/get.rs:65).Gaps found
1.
immutablemissing for Project/Domain/Role (the main gap) — implemented, see belowThe DB schema is already scaffolded and forgotten:
crates/resource-driver-sql/src/entity/project_option.rs—project_optiontable (
project_id,option_id,option_value), table creation wired atcrates/resource-driver-sql/src/lib.rs:299.crates/role-driver-sql/src/entity/role_option.rs—role_optiontable,same shape, table creation wired at
crates/role-driver-sql/src/lib.rs:327.domain_optiontable exists, and none is needed: Domain rowslive in the
projecttable (is_domain: booloncrates/resource-driver-sql/src/entity/project.rs:32), mirroring Python'sunified project/domain backend — so
project_optionalready covers both.Operator-configurability question this raises:
immutableis not aglobal 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
UserOptionsfields — still openTwo 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 ownpassword (admin/domain-admin reset still allowed), per Python's
LOCK_PASSWORD_OPTsemantics.ignore_change_password_upon_first_use— should be the per-user escapehatch from
[security_compliance] change_password_upon_first_use,mirroring
ignore_password_expiry/ignore_lockout_failure_attempts.(
multi_factor_auth_rules/multi_factor_auth_enabledenforcement isalready tracked separately in #264 and #266, not in scope here.)
3. Dead code — still open
crates/keystone/src/api/v3/user/types.rs:23-70is a fully commented-outduplicate of the
UserOptions/UserResponseconversions that now live incrates/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 optionsisremoved; the field now exists (see below).
Acceptance criteria
ProjectOptions/RoleOptionsadded tocore-types, each exposingimmutable: Option<bool>, following theUserOptionsshape. Domainreuses
ProjectOptions(it shares the underlyingprojectrow andproject_optiontable).project_option/role_optiontables, mirroring
crates/identity-driver-sql/src/user_option.rs'sto_model_iter/FromIteratorpattern (upsert viaON CONFLICT, bulkload_manymerge on list).optionsexposed on Project/Domain/Role create/update/response APItypes.
operation when
options.immutable == trueunless the same requestclears it (
ResourceProviderError::Immutable/RoleProviderError::Immutable→ 403 Forbidden), implemented inResourceService/RoleService.bootstrap admin project, and base roles (
admin/member/reader)should be created with
immutable: truefor parity. Deliberately notimplemented in this pass —
keystone-manage bootstraphere talks tothe 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_passwordblocks self-service password change; admin/domain-adminreset path unaffected. Tests for both.
ignore_change_password_upon_first_usecorrectly bypasses[security_compliance] change_password_upon_first_usethe same wayignore_password_expirybypassespassword_expires_days.crates/keystone/src/api/v3/user/types.rsremoved.
project.rs:112TODO resolved.Implemented on
claude/resource-options-support-qn3ilz(688d3ce).
References
doc/src/adr/0010-pci-dss-failed-auth-protection.mddoc/src/adr/0011-pci-dss-inactive-account-deactivation.mddoc/src/adr/0012-pci-dss-account-password-expiry.mdenforcement), PCI-DSS 8.2.4: Change passwords every 90 days #402 (password expiry follow-ups), Updating user password must reset failed auth counters #765 (failed-auth counter
reset on password change)