Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/assignment-driver-sql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ impl AssignmentBackend for SqlBackend {
if let Some(uid) = &params.user_id {
actors.push(uid.into());
}
if let Some(gid) = &params.group_id {
actors.push(gid.into());
}
if let Some(true) = &params.effective
&& let Some(uid) = &params.user_id
{
Expand Down
13 changes: 13 additions & 0 deletions crates/config/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,30 @@ pub struct TokenProvider {
/// compromised token.
#[serde(default = "default_token_expiration")]
pub expiration: usize,
/// 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,
}

fn default_token_expiration() -> usize {
3600
}

fn default_revoke_by_id() -> bool {
false
}

impl Default for TokenProvider {
fn default() -> Self {
Self {
provider: TokenProviderDriver::Fernet,
expiration: default_token_expiration(),
revoke_by_id: default_revoke_by_id(),
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions crates/core-types/src/assignment/assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ impl AssignmentCreate {
)
}

/// Instantiate GroupSystem assignment.
pub fn group_system<A, T, R>(actor_id: A, target_id: T, role_id: R, inherited: bool) -> Self
where
A: Into<String>,
T: Into<String>,
R: Into<String>,
{
Self::new(
actor_id,
target_id,
role_id,
AssignmentType::GroupSystem,
inherited,
)
}

/// Instantiate UserDomain assignment.
pub fn user_domain<A, T, R>(actor_id: A, target_id: T, role_id: R, inherited: bool) -> Self
where
Expand Down
59 changes: 54 additions & 5 deletions crates/core/src/assignment/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,26 @@ impl AssignmentApi for AssignmentService {
revoked_at: chrono::Utc::now(),
};

state
.provider
.get_revoke_provider()
.create_revocation_event(state, revocation_event)
.await?;
// 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 = state.config_manager.config.read().await.token.revoke_by_id;

if !is_group_assignment || revoke_by_id {
state
.provider
.get_revoke_provider()
.create_revocation_event(state, revocation_event)
.await?;
}

state
.event_dispatcher
Expand Down Expand Up @@ -409,6 +424,40 @@ mod tests {
backend_driver: Arc::new(backend),
};

assert!(provider.revoke_grant(&state, assignment).await.is_ok());
}
#[tokio::test]
async fn test_revoke_group_system_grant() {
let mut revoke_mock = MockRevokeProvider::default();
// Group assignments must NOT create revocation events by default
// (revoke_by_id = false). Token validation rebuilds assignments
// at validation time. Reference: Python Keystone bug #1662514
revoke_mock.expect_create_revocation_event().never();

let state = get_mocked_state(
None,
Some(Provider::mocked_builder().mock_revoke(revoke_mock)),
)
.await;

let mut backend = MockAssignmentBackend::default();
let assignment = AssignmentBuilder::default()
.actor_id("group_id")
.role_id("rid1")
.target_id("system")
.r#type(AssignmentType::GroupSystem)
.build()
.unwrap();
let assignment_clone = assignment.clone();
backend
.expect_revoke_grant()
.withf(move |_, params: &Assignment| *params == assignment_clone)
.returning(|_, _| Ok(()));

let provider = AssignmentService {
backend_driver: Arc::new(backend),
};

assert!(provider.revoke_grant(&state, assignment).await.is_ok());
}
}
23 changes: 23 additions & 0 deletions crates/keystone/src/api/v3/role_assignment/system/group/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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
//! # System Group role API
use utoipa_axum::router::OpenApiRouter;

use crate::keystone::ServiceState;

mod role;

pub(crate) fn openapi_router() -> OpenApiRouter<ServiceState> {
OpenApiRouter::new().merge(role::openapi_router())
}
Loading
Loading