Skip to content
Merged
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
128 changes: 81 additions & 47 deletions crates/assignment-driver-sql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,15 @@ inventory::submit! {
impl SqlBackend {
/// Resolve implied roles for a set of assignments.
///
/// Fetches role imply rules, computes transitive closure, and generates
/// assignment entries for each implied role. Does NOT resolve role names
/// (that's the provider's responsibility).
/// Walks the imply-rule graph breadth-first, starting only from the
/// role IDs present in `assignments` and fetching each frontier role's
/// direct rules via `list_role_imply_rules_by_prior`, instead of pulling
/// the entire `implied_role` table. Effective-role sets are typically a
/// handful of roles, so this trades one full-table scan for a few
/// narrow, indexed lookups. Computes transitive closure over the
/// resulting (small) subgraph, and generates assignment entries for
/// each implied role. Does NOT resolve role names (that's the
/// provider's responsibility).
///
/// Returns a `Vec<Assignment>` containing both the original assignments
/// and any additionally generated implied role assignments.
Expand All @@ -72,17 +78,33 @@ impl SqlBackend {
assignments: Vec<Assignment>,
) -> Result<Vec<Assignment>, AssignmentProviderError> {
let exec = ExecutionContext::internal(state);
let rules = state
.provider
.get_role_provider()
.list_role_imply_rules(&exec)
.await?;
let mut imply_rules: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for rule in &rules {
imply_rules
.entry(rule.prior_role.id.clone())
.or_default()
.insert(rule.implied_role.id.clone());
let mut visited: HashSet<String> = HashSet::new();
let mut frontier: BTreeSet<String> =
assignments.iter().map(|a| a.role_id.clone()).collect();
while !frontier.is_empty() {
let mut next_frontier: BTreeSet<String> = BTreeSet::new();
for role_id in frontier {
if !visited.insert(role_id.clone()) {
continue;
}
let rules = state
.provider
.get_role_provider()
.list_role_imply_rules_by_prior(&exec, &role_id)
.await?;
for rule in rules {
let implied_id = rule.implied_role.id.clone();
imply_rules
.entry(rule.prior_role.id.clone())
.or_default()
.insert(implied_id.clone());
if !visited.contains(&implied_id) {
next_frontier.insert(implied_id);
}
}
}
frontier = next_frontier;
}
// Transitive expansion
let mut changed = true;
Expand Down Expand Up @@ -415,23 +437,29 @@ mod tests {
.into_connection();

let mut role_mock = MockRoleProvider::default();
role_mock.expect_list_role_imply_rules().returning(|_e| {
Ok(vec![
RoleImplyBuilder::default()
.prior_role(RoleRef {
id: "1".into(),
name: Some("r1".into()),
domain_id: None,
})
.implied_role(RoleRef {
id: "2".into(),
name: Some("r2".into()),
domain_id: None,
})
.build()
.unwrap(),
])
});
role_mock
.expect_list_role_imply_rules_by_prior()
.returning(|_e, prior_role_id| {
if prior_role_id == "1" {
Ok(vec![
RoleImplyBuilder::default()
.prior_role(RoleRef {
id: "1".into(),
name: Some("r1".into()),
domain_id: None,
})
.implied_role(RoleRef {
id: "2".into(),
name: Some("r2".into()),
domain_id: None,
})
.build()
.unwrap(),
])
} else {
Ok(vec![])
}
});
let provider = Provider::mocked_builder()
.mock_role(role_mock)
.build()
Expand Down Expand Up @@ -649,23 +677,29 @@ mod tests {
.into_connection();

let mut role_mock = MockRoleProvider::default();
role_mock.expect_list_role_imply_rules().returning(|_e| {
Ok(vec![
RoleImplyBuilder::default()
.prior_role(RoleRef {
id: "1".into(),
name: Some("r1".into()),
domain_id: None,
})
.implied_role(RoleRef {
id: "2".into(),
name: Some("r2".into()),
domain_id: None,
})
.build()
.unwrap(),
])
});
role_mock
.expect_list_role_imply_rules_by_prior()
.returning(|_e, prior_role_id| {
if prior_role_id == "1" {
Ok(vec![
RoleImplyBuilder::default()
.prior_role(RoleRef {
id: "1".into(),
name: Some("r1".into()),
domain_id: None,
})
.implied_role(RoleRef {
id: "2".into(),
name: Some("r2".into()),
domain_id: None,
})
.build()
.unwrap(),
])
} else {
Ok(vec![])
}
});
let provider = Provider::mocked_builder()
.mock_role(role_mock)
.build()
Expand Down
93 changes: 30 additions & 63 deletions crates/core/src/token/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use openstack_keystone_core_types::auth::{
AuthzInfo, AuthzInfoBuilder, ScopeInfo, SecurityContext, TrustProjectInfo,
};
use openstack_keystone_core_types::events::{Event, EventPayload, Operation};
use openstack_keystone_core_types::resource::ResourceProviderError;
use openstack_keystone_core_types::resource::{Domain, ResourceProviderError};
use openstack_keystone_core_types::role::{Role, RoleListParameters, RoleRef};
use openstack_keystone_core_types::token::{
FernetToken, TokenRestriction, TokenRestrictionCreate, TokenRestrictionListParameters,
Expand Down Expand Up @@ -107,12 +107,28 @@ impl TokenService {

/// Build [`AuthzInfo`] from a token by fetching scope objects
/// from DB.
///
/// `known_domain` is the caller's already-resolved user domain: when a
/// project scope's `domain_id` matches it, that `Domain` is reused
/// instead of issuing a second `get_domain` lookup for the same row.
async fn build_authz_info_from_fernet_token(
&self,
state: &ServiceState,
token: &FernetToken,
known_domain: &Domain,
) -> Result<AuthzInfo, TokenProviderError> {
let ctx = ExecutionContext::internal(state);
let get_project_domain = async |domain_id: &str| -> Result<Domain, TokenProviderError> {
if domain_id == known_domain.id {
return Ok(known_domain.clone());
}
Ok(state
.provider
.get_resource_provider()
.get_domain(&ctx, domain_id)
.await?
.ok_or(ResourceProviderError::DomainNotFound(domain_id.to_owned()))?)
};
let scope = match token {
FernetToken::ApplicationCredential(data) => {
let project = state
Expand All @@ -123,29 +139,15 @@ impl TokenService {
.ok_or(ResourceProviderError::ProjectNotFound(
data.project_id.clone(),
))?;
let project_domain = state
.provider
.get_resource_provider()
.get_domain(&ctx, &project.domain_id)
.await?
.ok_or(ResourceProviderError::DomainNotFound(
project.domain_id.clone(),
))?;
let project_domain = get_project_domain(&project.domain_id).await?;
ScopeInfo::Project {
project,
project_domain,
}
}
FernetToken::DomainScope(data) => ScopeInfo::Domain(
state
.provider
.get_resource_provider()
.get_domain(&ctx, &data.domain_id)
.await?
.ok_or(ResourceProviderError::DomainNotFound(
data.domain_id.clone(),
))?,
),
FernetToken::DomainScope(data) => {
ScopeInfo::Domain(get_project_domain(&data.domain_id).await?)
}
FernetToken::ProjectScope(data) => {
let project = state
.provider
Expand All @@ -155,29 +157,15 @@ impl TokenService {
.ok_or(ResourceProviderError::ProjectNotFound(
data.project_id.clone(),
))?;
let project_domain = state
.provider
.get_resource_provider()
.get_domain(&ctx, &project.domain_id)
.await?
.ok_or(ResourceProviderError::DomainNotFound(
project.domain_id.clone(),
))?;
let project_domain = get_project_domain(&project.domain_id).await?;
ScopeInfo::Project {
project,
project_domain,
}
}
FernetToken::FederationDomainScope(data) => ScopeInfo::Domain(
state
.provider
.get_resource_provider()
.get_domain(&ctx, &data.domain_id)
.await?
.ok_or(ResourceProviderError::DomainNotFound(
data.domain_id.clone(),
))?,
),
FernetToken::FederationDomainScope(data) => {
ScopeInfo::Domain(get_project_domain(&data.domain_id).await?)
}
FernetToken::FederationProjectScope(data) => {
let project = state
.provider
Expand All @@ -187,14 +175,7 @@ impl TokenService {
.ok_or(ResourceProviderError::ProjectNotFound(
data.project_id.clone(),
))?;
let project_domain = state
.provider
.get_resource_provider()
.get_domain(&ctx, &project.domain_id)
.await?
.ok_or(ResourceProviderError::DomainNotFound(
project.domain_id.clone(),
))?;
let project_domain = get_project_domain(&project.domain_id).await?;
ScopeInfo::Project {
project,
project_domain,
Expand All @@ -210,14 +191,7 @@ impl TokenService {
.ok_or(ResourceProviderError::ProjectNotFound(
data.project_id.clone(),
))?;
let project_domain = state
.provider
.get_resource_provider()
.get_domain(&ctx, &project.domain_id)
.await?
.ok_or(ResourceProviderError::DomainNotFound(
project.domain_id.clone(),
))?;
let project_domain = get_project_domain(&project.domain_id).await?;
let trust = state
.provider
.get_trust_provider()
Expand All @@ -239,14 +213,7 @@ impl TokenService {
.ok_or(ResourceProviderError::ProjectNotFound(
data.project_id.clone(),
))?;
let project_domain = state
.provider
.get_resource_provider()
.get_domain(&ctx, &project.domain_id)
.await?
.ok_or(ResourceProviderError::DomainNotFound(
project.domain_id.clone(),
))?;
let project_domain = get_project_domain(&project.domain_id).await?;
ScopeInfo::Project {
project,
project_domain,
Expand Down Expand Up @@ -371,14 +338,14 @@ impl TokenService {
UserIdentityInfoBuilder::default()
.user_id(token.user_id())
.user(user.clone())
.user_domain(user_domain)
.user_domain(user_domain.clone())
.build()?,
),
})
.expires_at(*token.expires_at())
.authorization(
// populate scope info
self.build_authz_info_from_fernet_token(state, &token)
self.build_authz_info_from_fernet_token(state, &token, &user_domain)
.await?,
);
if let FernetToken::Restricted(restriction) = &token {
Expand Down
Loading
Loading