One access policy from content; onboarding into Kanidm as a form
Who may do what to what was five checks in five files: `qualifies` on
a page, `requires_group`/`public` on a resource, a desk's transition
allow-list, a self_transition's email check, the automation token.
They are now compiled from content on every load into one Cedar
policy set (src/access.rs), and every server entry point asks it: open
and submit a page, read a resource, move a record, fire a self
transition, read a bucket as the automation service, extend an
invite. Each decision names the rules that granted it; refusals and
every mutation are published on portal.access.decided, and
portal.access.<site>.may answers the same question for other apps
over request-reply (proven live against uhhm's content). The policy
validates against a fixed schema, so a content shape the compiler
cannot express fails instead of granting nothing. Identity is still
Kanidm's; the browser keeps is_qualified as a rendering hint only.
`invite: {group}` on an alternative of a gated page onboards a person:
src/people.rs creates the account, records the email, adds the group
and mints a one-time credential link through Kanidm's REST API with a
service-account token (KANIDM_API_TOKEN, in idm_people_on_boarding).
The result is recorded with the answer into the runtime's
portal_invites bucket and announced on portal.people.invited. Validated:
an invite needs a gated page and required name and email fields.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
7408fa972a
commit
eeeb89b217
+30
-16
@@ -38,7 +38,7 @@ pub async fn get_resource(
|
||||
.resource
|
||||
.as_ref()
|
||||
.ok_or_else(|| ServerFnError::new("feature has no resource"))?;
|
||||
authorize_resource(resource).await?;
|
||||
authorize_resource(&state, &question_id, &alternative, &feature_name).await?;
|
||||
fetch_resource_value(&state, resource, ¶ms).await
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ pub async fn get_requirement_options(
|
||||
.resource
|
||||
.as_ref()
|
||||
.ok_or_else(|| ServerFnError::new("requirement has no resource"))?;
|
||||
authorize_resource(resource).await?;
|
||||
authorize_resource(&state, &question_id, &alternative, &format!("{feature_name}|{requirement_name}")).await?;
|
||||
fetch_resource_value(&state, resource, ¶ms).await
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ pub async fn get_requirement_binding(
|
||||
.bind
|
||||
.as_ref()
|
||||
.ok_or_else(|| ServerFnError::new("requirement has no binding"))?;
|
||||
authorize_resource(&bind.resource).await?;
|
||||
authorize_resource(&state, &question_id, &alternative, &format!("{feature_name}|{requirement_name}|bind")).await?;
|
||||
fetch_resource_value(&state, &bind.resource, ¶ms).await
|
||||
}
|
||||
|
||||
@@ -152,26 +152,40 @@ fn find_feature(
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn authorize_resource(resource: &crate::content::ResourceSpec) -> Result<(), ServerFnError> {
|
||||
async fn authorize_resource(
|
||||
state: &crate::server::AppState,
|
||||
question_id: &str,
|
||||
alternative: &str,
|
||||
feature: &str,
|
||||
) -> Result<(), ServerFnError> {
|
||||
use crate::access::{decide_and_publish, Action, Principal, Resource};
|
||||
use crate::auth::{User, SESSION_USER_KEY};
|
||||
|
||||
if resource.public {
|
||||
return Ok(());
|
||||
}
|
||||
let group = resource
|
||||
.requires_group
|
||||
.as_deref()
|
||||
.ok_or_else(|| ServerFnError::new("resource is not accessible"))?;
|
||||
let session: tower_sessions::Session = leptos_axum::extract().await?;
|
||||
let user = session
|
||||
.get::<User>(SESSION_USER_KEY)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?
|
||||
.ok_or_else(|| ServerFnError::new("not signed in"))?;
|
||||
if !user.groups.iter().any(|g| g == group) {
|
||||
return Err(ServerFnError::new("not authorized"));
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let resource = Resource::Feature {
|
||||
page: question_id.to_string(),
|
||||
alternative: alternative.to_string(),
|
||||
feature: feature.to_string(),
|
||||
};
|
||||
let decision = decide_and_publish(
|
||||
&state.nats,
|
||||
&state.access.load(),
|
||||
&Principal::from_user(user.as_ref()),
|
||||
Action::Read,
|
||||
&resource,
|
||||
serde_json::json!({}),
|
||||
);
|
||||
if decision.allow {
|
||||
Ok(())
|
||||
} else if user.is_none() {
|
||||
Err(ServerFnError::new("not signed in"))
|
||||
} else {
|
||||
Err(ServerFnError::new("not authorized"))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
|
||||
Reference in New Issue
Block a user