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
60 lines
2.6 KiB
Rust
60 lines
2.6 KiB
Rust
#![cfg(feature = "ssr")]
|
|
|
|
pub mod oidc;
|
|
|
|
use arc_swap::ArcSwap;
|
|
use axum::extract::FromRef;
|
|
use leptos::prelude::LeptosOptions;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use crate::content::Question;
|
|
|
|
#[derive(Clone)]
|
|
pub struct AppState {
|
|
pub leptos_options: LeptosOptions,
|
|
pub nats: async_nats::Client,
|
|
/// JetStream context - source of every NATS KV bucket this app
|
|
/// reads/writes (applicants, and whatever a `ResourceSpec` names).
|
|
pub jetstream: async_nats::jetstream::Context,
|
|
/// Swapped out wholesale on a `content::CONTENT_RELOAD_SUBJECT`
|
|
/// message (see `content::watch_for_reload`) - readers never hold
|
|
/// a lock, just an atomic pointer load, so a reload never blocks or
|
|
/// is blocked by an in-flight request.
|
|
pub questions: Arc<ArcSwap<HashMap<String, Question>>>,
|
|
/// Every bucket's event-sourced state graph, loaded from
|
|
/// `aggregates.yaml` (see `content::load_aggregates_from_gitea`) and
|
|
/// hot-swapped alongside `questions` on the same reload. A bucket
|
|
/// with no entry here isn't event-sourced - plain KV mutate-in-place
|
|
/// still works (see `answers.rs`).
|
|
pub aggregates: Arc<ArcSwap<HashMap<String, crate::aggregates::AggregateSchema>>>,
|
|
/// Site branding from the content repo's optional `site.yaml`
|
|
/// (title, wordmark, hero kind) - hot-swapped with `questions`/
|
|
/// `aggregates` on the same reload. Default = the historical uhhm
|
|
/// look.
|
|
pub site: Arc<ArcSwap<crate::content::SiteConfig>>,
|
|
/// The access policy compiled from the content above (see
|
|
/// `access.rs`), swapped with it on every reload. Every server entry
|
|
/// point decides through this and nothing else.
|
|
pub access: Arc<ArcSwap<crate::access::Policy>>,
|
|
/// `scheme://host` of the Gitea instance content is loaded from
|
|
/// (see `content::gitea_api_base`) - kept alongside `questions`
|
|
/// rather than re-derived per call, since `resolve_gitea_repo` needs
|
|
/// it too and has no other reason to see `CONTENT_REPO` itself.
|
|
pub gitea_base: String,
|
|
/// `{gitea}/api/v1/repos/{owner}/{repo}/raw` and the branch - what
|
|
/// `content::site_asset_handler` proxies content-shipped assets from.
|
|
pub content_raw_base: String,
|
|
pub content_branch: String,
|
|
pub oidc: Arc<oidc::Oidc>,
|
|
/// `None` when `GARAGE_*` env vars aren't set - uploads are the one
|
|
/// optional feature, everything else works without Garage.
|
|
pub garage: Option<crate::upload::Garage>,
|
|
}
|
|
|
|
impl FromRef<AppState> for LeptosOptions {
|
|
fn from_ref(state: &AppState) -> Self {
|
|
state.leptos_options.clone()
|
|
}
|
|
}
|