Skip to content

Feature/system assignment group - #796

Open
konac-hamza wants to merge 1 commit into
openstack-experimental:mainfrom
konac-hamza:feature/system-assignment-group
Open

konac-hamza wants to merge 1 commit into
openstack-experimental:mainfrom
konac-hamza:feature/system-assignment-group

Conversation

@konac-hamza

@konac-hamza konac-hamza commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

#757 System group role assignment API

This PR covers CRUD operations for system role assignments of group actors.

  • API endpoints and test methods are mirrored from the system user role assignment implementation
  • OPA policy rules are mirrored from the system user role policy package
  • Integration tests are implemented for all endpoints
  • The following bug is fixed in a separate commit:

When listing roles for a system group, the API was returning all system role assignments in the database instead of only those belonging to the requested group. The root cause was that group_id was never added to the actors filter in list_assignments, while user_id was handled correctly. With an empty actors list, the database query had no filter and returned every system assignment. The fix was adding group_id to the actors list the same way user_id is handled.

  • The following bug is fixed in a separate commit:

When revoking a group role assignment, the implementation was creating a revocation event with only role_id set and no user_id. This caused all tokens holding that role to be invalidated, not just tokens of users in the revoked group. This matches Python Keystone bug #1662514, which was fixed by only creating revocation events for group assignments when revoke_by_id = true is set in config (default: false). The fix adds revoke_by_id to the token config and skips revocation events for group assignments by default.

Known Limitations

Effective role listing with group_id and effective=true is not yet fully implemented. Python Keystone handles this case by expanding the group into its members via list_users_in_group and including their direct role assignments in the result. The Rust identity backend does not yet implement list_users_in_group, so this expansion is currently missing. This will be tracked and implemented separately.

@konac-hamza
konac-hamza force-pushed the feature/system-assignment-group branch 2 times, most recently from 9289461 to b3a55d7 Compare June 24, 2026 21:26
@konac-hamza
konac-hamza marked this pull request as draft June 24, 2026 22:17
@konac-hamza
konac-hamza requested a review from gtema June 24, 2026 22:31
@gtema

gtema commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

@konac-hamza are you still working on the PR?

@konac-hamza

Copy link
Copy Markdown
Collaborator Author

@konac-hamza are you still working on the PR?

Because of you closed the sub-issue, I am not working. Should I complete the PR?

@gtema

gtema commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

ah, there was an accidental duplication, issue #755 describes what is still missing for system assignments. So yes, please, let us complete system role assignments to users/groups

@konac-hamza

Copy link
Copy Markdown
Collaborator Author

ah, there was an accidental duplication, issue #755 describes what is still missing for system assignments. So yes, please, let us complete system role assignments to users/groups

I will start the PR as soon as possible. Thanks for your notify.

@konac-hamza
konac-hamza force-pushed the feature/system-assignment-group branch from b3a55d7 to 0810e33 Compare September 7, 2026 21:46
@konac-hamza
konac-hamza marked this pull request as ready for review September 7, 2026 21:47
@konac-hamza
konac-hamza force-pushed the feature/system-assignment-group branch from 0810e33 to 9f3d930 Compare September 7, 2026 21:49
Signed-off-by: Hamza Konac <hamza.konac@tubitak.gov.tr>
@konac-hamza
konac-hamza force-pushed the feature/system-assignment-group branch from 9f3d930 to 0cb661b Compare September 8, 2026 21:30

@gtema gtema left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the formatting of the code is very broken

@gtema gtema left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh sorry, this is the new styling in github that literally make impression of empty lines between every change line

@gtema gtema left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review (caveman-review). Core change — guarding the group-grant revocation event — is sound and matches Python Keystone's post-#1662514 behavior. Main issues: (1) the revoke_by_id=true path re-introduces the over-broad revocation the Python bug fix removed and is not equivalent to Python's option of the same name; (2) the change silently alters revoke behavior for the existing GroupDomain/GroupProject paths; (3) no unit test for the new branch; plus a duplicated license header in list.rs and assorted nits. Details inline.

Comment on lines +15 to +29
//! System group role: list.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate Apache license header and //! System group role: list. doc comment (lines 15-29 repeat lines 1-13 + 15). Delete lines 15-29.

let group = group?.ok_or_else(|| {
info!("Group {} was not found", group_id);
KeystoneApiError::NotFound {
resource: "group".into(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resource: "group" here, but grant/check/revoke in this PR all use resource: "grant" for the same not-found case. Pick one.

identifier: "" (all four handlers) drops the id from the 404 body and logs — pass group_id / role_id.

let roles: Vec<Role> = assignments
.into_iter()
.map(|a| a.try_into())
.collect::<Result<std::collections::HashSet<_>, _>>()?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HashSet<Role>Vec makes the role order in the response non-deterministic across calls. Collect, then sort by id before returning for a stable response.

tag="role_assignments"
)]
#[tracing::instrument(
name = "api::system_group_role_list",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: instrument name api::system_group_role_list (also _check) vs api::v3::system_group_role_grant / _revoke. Unify the prefix.

let query_params = RoleAssignmentListParameters {
group_id: Some(group_id.clone()),
system_id: Some("system".into()),
effective: Some(true),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: effective: Some(true) here vs Some(false) in list.rs. Group effective-expansion is the known-unimplemented path (list_users_in_group), so effective=true is a no-op for a group actor today. Align to false or add a comment explaining why check differs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed the system user assignment handlers where check.rs uses Some(true) and list.rs uses Some(false). Since effective expansion for groups is currently a no-op (as you noted), aligning both to Some(false) would be more accurate. I'll update check.rs to use Some(false) and add a comment.

)
.await?;

let grant = AssignmentBuilder::default()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No pre-revoke existence check. Python delete_grant calls check_grant_role_id first and raises RoleAssignmentNotFound (404) when the group holds no such grant (keystone/assignment/core.py, test_backends.py assert_role_assignment_not_found_exception). Here the handler fetches group + role, enforces policy, then calls revoke_grant unconditionally → 204 for a no-op revoke. Diverges from Python and from REST norms. Confirm whether the existing system/user revoke handler has the same gap before sizing the fix.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked Python and read methods which you mentioned. In Rust there is no provider method that check assignment directly, so should I check them like how check.rs handle or implement provider method? also Should I fix gap in system/user revoke handler?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check from api is implemented as a narrow list and filter. I guess it would make sense to implement a new shortcut in the provider, so yes, please create the dedicated check method which works like current check API methods

Comment on lines +816 to +846
// Only create revocation event for group assignments if revoke_by_id is enabled.
// By default group revocations do not create revocation events since token
// validation rebuilds assignments at validation time.
// Reference: Python Keystone bug #1662514
let is_group_assignment = matches!(
&grant.r#type,
AssignmentType::GroupDomain
| AssignmentType::GroupProject
| AssignmentType::GroupSystem
);

let revoke_by_id = ctx
.state()
.config_manager
.config
.read()
.await
.token
.revoke_by_id;
if !is_group_assignment || revoke_by_id {
// ADR 0034 §4: the central revocation event stays on the global revoke
// provider, unrouted — it is not an assignment-backend operation.
ctx.state()
.provider
.get_revoke_provider()
.create_revocation_event(ctx, revocation_event)
.await?;
// ADR 0031 "Tokens": revoking a grant cascades revocation of every
// token carrying that role - `"cascade"`, not a direct user request.
crate::token::TOKEN_METRICS.revoked_total.inc(["cascade"]);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-checked against Python Keystone (opendev.org/openstack/keystone @ 5f1d13f38), bug #1662514, fix 9a4391c49.

The diagnosis is correct: Rust currently builds an over-broad revoke event for group grants (RevocationEventCreate { user_id: None, role_id: Some, project/domain } at L802) — exactly the pre-2017 Python bug. Guarding it is the right fix, and validation-time recomputation makes it safe: calculate_effective_roles (auth.rs:666) runs every validation and the SQL driver expands user_id → list_groups_of_user → group actors for domain/project/system scope (assignment-driver-sql/src/lib.rs:289). The PR's "known limitation" is the reverse direction (group → members) and does not affect token validation.

Three problems:

  1. 🔴 revoke_by_id=true is not equivalent to Python's revoke_by_id=True — it re-introduces the bug. Modern Python gates only _invalidate_token_cache(...) on the flag: a blanket, non-destructive cache flush (still-valid tokens survive; test_v3_auth.py token3 now expects OK). Python never re-persists the broad (scope, role) event, flag on or off. This true branch calls create_revocation_event with user_id: None → a hard revocation via is_token_revoked that permanently kills every token carrying that role on that scope, including users with a direct assignment. Enabling the flag = "revert #1662514". Fix: resolve group members and emit per-user-scoped events, or rename/document the flag as "restores broad scope+role revocation for group grants; NOT equivalent to Python [token] revoke_by_id".

  2. Scope creep. is_group_assignment also covers GroupDomain | GroupProject, changing revoke behavior for pre-existing endpoints, not just the new system/group one. Call this out in the PR description and in the security-model / ADR 0031 — revocation is a MUST-READ area.

  3. No test for the new branch. test_revoke_grant (L448) only exercises UserProject. Add: GroupSystem + revoke_by_id=falsecreate_revocation_event not called; GroupProject same; any group + revoke_by_id=true → event created; UserProject unchanged.

| AssignmentType::GroupSystem
);

let revoke_by_id = ctx

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading config.read().await.token.revoke_by_id per revoke is fine. But note the default (false, config/token.rs) is the opposite of Python's (keystone/conf/token.py:82default=True). false is the right call for Rust since the true path is the buggy one — but the PR description's "(default: false)" claim about Python is wrong; state the real reason Rust diverges.

Comment on lines +37 to +44
/// Controls whether token revocation is enabled for group role
/// assignments. When disabled (default), group role revocations do not
/// create revocation events since token validation rebuilds assignments
/// at validation time. Enabling this creates revocation events for group
/// assignments but may cause overly broad token invalidation.
/// Matches Python Keystone's [token] revoke_by_id option.
#[serde(default = "default_revoke_by_id")]
pub revoke_by_id: bool,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Matches Python Keystone's [token] revoke_by_id option" is misleading:

  • Python default is True, not false.
  • Python's flag gates a non-destructive token-cache flush; it never persists the broad (scope, role) revocation event. Here revoke_by_id=true re-enables exactly that broad event (see service.rs comment). The Rust flag means "restore broad scope+role revocation for group grants", which is the pre-#1662514 behavior, not Python parity.

Also: this option is not surfaced in the sample / reference config. Add it next to [token] expiration with help text that describes the actual Rust behavior.

@@ -0,0 +1,29 @@
# METADATA
# description: Policy for checking group roles on system
package identity.system.group.role.check

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new policy files use 8-space indentation; the rest of policy/ uses tabs. check_test.rego, grant.rego, list.rego, revoke.rego also lack a trailing newline. opa fmt --diff in pre-commit will fail. Run opa fmt -w policy/resource/system/group/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants