first pass at access control setup

This commit is contained in:
2026-03-24 14:45:07 -05:00
parent af5175b96a
commit 2ebb03b868
105 changed files with 6163 additions and 1416 deletions

View File

@@ -3,7 +3,10 @@
use attune_common::{
config::LdapConfig,
repositories::{
identity::{CreateIdentityInput, IdentityRepository, UpdateIdentityInput},
identity::{
CreateIdentityInput, IdentityRepository, IdentityRoleAssignmentRepository,
UpdateIdentityInput,
},
Create, Update,
},
};
@@ -63,6 +66,11 @@ pub async fn authenticate(
// Upsert identity in DB and issue JWT tokens
let identity = upsert_identity(state, &claims).await?;
if identity.frozen {
return Err(ApiError::Forbidden(
"Identity is frozen and cannot authenticate".to_string(),
));
}
let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;
@@ -351,10 +359,13 @@ async fn upsert_identity(
display_name,
password_hash: None,
attributes: Some(attributes),
frozen: None,
};
IdentityRepository::update(&state.db, identity.id, updated)
let identity = IdentityRepository::update(&state.db, identity.id, updated)
.await
.map_err(Into::into)
.map_err(ApiError::from)?;
sync_roles(&state.db, identity.id, "ldap", &claims.groups).await?;
Ok(identity)
}
None => {
// Avoid login collisions
@@ -363,7 +374,7 @@ async fn upsert_identity(
None => desired_login,
};
IdentityRepository::create(
let identity = IdentityRepository::create(
&state.db,
CreateIdentityInput {
login,
@@ -373,11 +384,24 @@ async fn upsert_identity(
},
)
.await
.map_err(Into::into)
.map_err(ApiError::from)?;
sync_roles(&state.db, identity.id, "ldap", &claims.groups).await?;
Ok(identity)
}
}
}
async fn sync_roles(
db: &sqlx::PgPool,
identity_id: i64,
source: &str,
roles: &[String],
) -> Result<(), ApiError> {
IdentityRoleAssignmentRepository::replace_managed_roles(db, identity_id, source, roles)
.await
.map_err(Into::into)
}
/// Derive the login name from LDAP claims.
fn derive_login(claims: &LdapUserClaims) -> String {
claims

View File

@@ -3,7 +3,10 @@
use attune_common::{
config::OidcConfig,
repositories::{
identity::{CreateIdentityInput, IdentityRepository, UpdateIdentityInput},
identity::{
CreateIdentityInput, IdentityRepository, IdentityRoleAssignmentRepository,
UpdateIdentityInput,
},
Create, Update,
},
};
@@ -282,6 +285,11 @@ pub async fn handle_callback(
}
let identity = upsert_identity(state, &oidc_claims).await?;
if identity.frozen {
return Err(ApiError::Forbidden(
"Identity is frozen and cannot authenticate".to_string(),
));
}
let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;
@@ -511,10 +519,13 @@ async fn upsert_identity(
display_name,
password_hash: None,
attributes: Some(attributes.clone()),
frozen: None,
};
IdentityRepository::update(&state.db, identity.id, updated)
let identity = IdentityRepository::update(&state.db, identity.id, updated)
.await
.map_err(Into::into)
.map_err(ApiError::from)?;
sync_roles(&state.db, identity.id, "oidc", &oidc_claims.groups).await?;
Ok(identity)
}
None => {
let login = match IdentityRepository::find_by_login(&state.db, &desired_login).await? {
@@ -522,7 +533,7 @@ async fn upsert_identity(
None => desired_login,
};
IdentityRepository::create(
let identity = IdentityRepository::create(
&state.db,
CreateIdentityInput {
login,
@@ -532,11 +543,24 @@ async fn upsert_identity(
},
)
.await
.map_err(Into::into)
.map_err(ApiError::from)?;
sync_roles(&state.db, identity.id, "oidc", &oidc_claims.groups).await?;
Ok(identity)
}
}
}
async fn sync_roles(
db: &sqlx::PgPool,
identity_id: i64,
source: &str,
roles: &[String],
) -> Result<(), ApiError> {
IdentityRoleAssignmentRepository::replace_managed_roles(db, identity_id, source, roles)
.await
.map_err(Into::into)
}
fn derive_login(oidc_claims: &OidcIdentityClaims) -> String {
oidc_claims
.email