#![allow(clippy::result_large_err)]
use std::sync::Arc;
use std::time::Instant;
use gunnar_proto::GrantLevel;
use tonic::{Request, Status};
use crate::auth::{audit_allowed, audit_denied, Access, AuthMode, Caller};
use crate::bus::EventBus;
use crate::config::ServerConfig;
use crate::seam::ControlPlane;
pub mod access;
pub mod accounts;
pub mod admin;
#[cfg(feature = "metered")]
pub mod billing;
pub mod bootstrap;
pub mod clans;
#[cfg(feature = "metered")]
pub mod entitlement;
#[cfg(feature = "metered")]
pub mod sealed;
pub mod enterprise;
pub mod events;
pub mod forge;
pub mod meters;
pub mod mirror;
pub mod policy;
pub mod pulls;
pub mod refs;
pub mod repos;
pub mod sso;
pub mod usage;
pub mod usage_batch;
pub mod webhook;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Listing {
Everything,
PublicSubset,
}
#[derive(Clone)]
pub struct ServiceContext {
pub control: Arc<dyn ControlPlane>,
pub bus: Arc<EventBus>,
pub config: ServerConfig,
pub started: Instant,
pub transfer_gate: Option<Arc<dyn crate::TransferGate>>,
pub browse: Option<Arc<std::sync::atomic::AtomicBool>>,
pub audit: Option<Arc<dyn crate::auth::AuditLedger>>,
pub unbound: Arc<crate::auth::UnboundCredentialLog>,
pub heartbeats: Arc<crate::heartbeat::Registry>,
pub callers: Arc<crate::topology::CallerLog>,
pub replay: Arc<crate::auth::ReplayGuard>,
pub web_challenges: Arc<crate::webauthn::Challenges>,
pub web_sessions: Arc<WebSessions>,
pub machine_enrolments: Arc<crate::machine_enrolment::Requests>,
pub sso: Arc<crate::sso::SsoState>,
pub sells_room: Arc<std::sync::atomic::AtomicBool>,
pub handlers: Option<tokio::runtime::Handle>,
}
pub const BROWSE_STILL_ANSWERS: &[&str] = &[
"Access.ListGrants",
"Access.ListGroupMembers",
"Access.ListGroupMembershipEvents",
"Access.ListKeys",
"Access.ListPrincipals",
"Mirror.ReplicationStatus",
"Mirror.LogFetch",
"Mirror.SnapshotFetch",
"Webhooks.ListWebhooks",
"Mirror.SetRole",
"Repos.Delete",
"Repos.Create",
];
pub const TWIN_STILL_ANSWERS: &[&str] = &["Admin.GrowStart", "Admin.GrowResume", "Admin.GrowStatus"];
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Standing {
pub account: String,
pub role: &'static str,
pub admin: String,
}
impl ServiceContext {
pub fn new(control: Arc<dyn ControlPlane>, bus: Arc<EventBus>, config: ServerConfig) -> Self {
Self {
control,
bus,
config,
started: Instant::now(),
transfer_gate: None,
browse: None,
audit: None,
unbound: Arc::new(crate::auth::UnboundCredentialLog::default()),
heartbeats: Arc::new(crate::heartbeat::Registry::default()),
callers: Arc::new(crate::topology::CallerLog::default()),
replay: Arc::new(crate::auth::ReplayGuard::default()),
web_challenges: Arc::new(crate::webauthn::Challenges::default()),
web_sessions: Arc::new(WebSessions::default()),
machine_enrolments: Arc::new(crate::machine_enrolment::Requests::default()),
sso: Arc::new(crate::sso::SsoState::default()),
sells_room: Arc::new(std::sync::atomic::AtomicBool::new(false)),
handlers: None,
}
}
pub fn with_sells_room(mut self, flag: Arc<std::sync::atomic::AtomicBool>) -> Self {
self.sells_room = flag;
self
}
pub fn with_handler_runtime(mut self, handlers: tokio::runtime::Handle) -> Self {
self.handlers = Some(handlers);
self
}
pub fn with_audit_ledger(mut self, ledger: Arc<dyn crate::auth::AuditLedger>) -> Self {
self.audit = Some(ledger);
self
}
pub fn with_heartbeats(mut self, registry: Arc<crate::heartbeat::Registry>) -> Self {
self.heartbeats = registry;
self
}
pub fn audit_change(&self, caller: &Caller, access: Access, level: GrantLevel, method: &'static str, what: &str) {
crate::auth::audit_change(method, caller, &access, level, self.config.auth, what, self.ledger());
}
pub fn audit_change_durable(&self, caller: &Caller, access: Access, level: GrantLevel, method: &'static str, what: &str) -> Result<(), Status> {
crate::auth::audit_change_durable(method, caller, &access, level, self.config.auth, what, self.ledger())
}
fn ledger(&self) -> Option<&dyn crate::auth::AuditLedger> {
self.audit.as_deref()
}
pub fn with_browse_flag(mut self, browse: Arc<std::sync::atomic::AtomicBool>) -> Self {
self.browse = Some(browse);
self
}
pub fn is_browse(&self) -> bool {
self.browse
.as_ref()
.is_some_and(|flag| flag.load(std::sync::atomic::Ordering::SeqCst))
}
fn refuse_on_browse(
&self,
caller: &Caller,
level: GrantLevel,
method: &'static str,
) -> Result<(), Status> {
if level == GrantLevel::Read || !self.is_browse() {
return Ok(());
}
let allowed = match method {
"Repos.Delete" | "Repos.Create" => caller.machine,
other => BROWSE_STILL_ANSWERS.contains(&other) || (TWIN_STILL_ANSWERS.contains(&other) && self.control.is_twin()),
};
if allowed {
return Ok(());
}
tracing::info!(
target: crate::auth::AUDIT_TARGET,
event = "control.browse_refused",
method,
principal = %caller.principal_id,
machine = caller.machine,
"a browse replica refused a control-plane mutator"
);
Err(Status::failed_precondition(format!(
"this box is a browse replica: it holds public repositories read-only and cannot be \
promoted or changed here — {method} is refused; run it against the primary"
)))
}
pub fn with_transfer_gate(mut self, gate: Arc<dyn crate::TransferGate>) -> Self {
self.transfer_gate = Some(gate);
self
}
pub fn caller<T>(&self, request: &Request<T>) -> Result<Caller, Status> {
if let Some(caller) = request.extensions().get::<Caller>() {
if caller.machine && caller.authenticated {
let address = request.remote_addr().map(|a| a.to_string()).unwrap_or_default();
self.callers.note(&caller.principal_id, &address, estate_clock::now_unix_ms(), false);
}
return Ok(caller.clone());
}
if self.config.auth == AuthMode::AllowAllInsecure {
return Ok(Caller {
principal_id: Caller::INSECURE_PRINCIPAL.to_owned(),
fingerprint: String::new(),
machine: false,
authenticated: false,
});
}
Err(crate::auth::AuthFailure::Missing.status())
}
pub fn is_self(&self, caller: &Caller, target: &str) -> bool {
caller.authenticated
&& !caller.principal_id.is_empty()
&& caller.principal_id == target
&& caller.principal_id != Caller::INSECURE_PRINCIPAL
}
pub fn require_self(
&self,
caller: &Caller,
level: GrantLevel,
method: &'static str,
) -> Result<(), Status> {
self.refuse_on_browse(caller, level, method)?;
if self.config.auth != AuthMode::AllowAllInsecure {
match self.control.narrowed_methods(&caller.principal_id) {
Ok(Some(allowed)) if !allowed.iter().any(|m| m == method) => {
audit_denied(
method,
caller,
&Access::Server,
level,
self.config.auth,
"outside its narrowed grant",
self.ledger(),
);
return Err(Status::permission_denied(format!(
"principal {} is narrowed to [{}]: {method} is outside its narrowed grant",
caller.principal_id,
allowed.join(", ")
)));
}
Ok(_) => {}
Err(err) => {
return Err(Status::unavailable(format!(
"the identity table could not be read: {err}"
)))
}
}
}
crate::auth::audit_self(method, caller, self.config.auth, self.ledger());
Ok(())
}
pub fn check(
&self,
caller: &Caller,
access: Access,
level: GrantLevel,
) -> Result<Option<String>, Status> {
if level != GrantLevel::Read && self.is_browse() {
return Ok(Some(
"this box is a browse replica: it holds public repositories read-only, and \
nothing above Read is done here; run it against the primary"
.to_owned(),
));
}
if self.config.auth == AuthMode::AllowAllInsecure {
return Ok(None);
}
match self.control.narrowed_methods(&caller.principal_id) {
Ok(Some(allowed)) => {
return Ok(Some(format!(
"principal {} is narrowed to [{}]: its grants are not consulted",
caller.principal_id,
allowed.join(", ")
)))
}
Ok(None) => {}
Err(err) => {
return Err(Status::unavailable(format!(
"the identity table could not be read: {err}"
)))
}
}
match self.control.may(&caller.principal_id, &access, level) {
Ok(true) => Ok(None),
Ok(false) => Ok(Some(format!(
"principal {} has no {} grant on {access}",
caller.principal_id,
crate::auth::level_name(level)
))),
Err(err) => Err(Status::unavailable(format!(
"the identity table could not be read: {err}"
))),
}
}
pub fn require(
&self,
caller: &Caller,
access: Access,
level: GrantLevel,
method: &'static str,
) -> Result<(), Status> {
self.refuse_on_browse(caller, level, method)?;
if self.config.auth != AuthMode::AllowAllInsecure {
match self.control.narrowed_methods(&caller.principal_id) {
Ok(Some(allowed)) => {
if allowed.iter().any(|m| m == method) {
audit_allowed(method, caller, &access, level, self.config.auth, self.ledger());
return Ok(());
}
audit_denied(method, caller, &access, level, self.config.auth, "outside its narrowed grant", self.ledger());
return Err(Status::permission_denied(format!(
"principal {} is narrowed to [{}]: {method} is outside its narrowed grant",
caller.principal_id,
allowed.join(", ")
)));
}
Ok(None) => {}
Err(err) => {
audit_denied(method, caller, &access, level, self.config.auth, "grant lookup failed", self.ledger());
return Err(Status::unavailable(format!("the identity table could not be read: {err}")));
}
}
}
if self.config.auth == AuthMode::AllowAllInsecure {
audit_allowed(
method,
caller,
&access,
level,
self.config.auth,
self.ledger(),
);
return Ok(());
}
match self.control.may(&caller.principal_id, &access, level) {
Ok(true) => {
audit_allowed(
method,
caller,
&access,
level,
self.config.auth,
self.ledger(),
);
Ok(())
}
Ok(false) => {
audit_denied(
method,
caller,
&access,
level,
self.config.auth,
"no grant",
self.ledger(),
);
Err(Status::permission_denied(format!(
"principal {} has no {} grant on {access}",
caller.principal_id,
crate::auth::level_name(level)
)))
}
Err(err) => {
audit_denied(
method,
caller,
&access,
level,
self.config.auth,
"grant lookup failed",
self.ledger(),
);
Err(Status::unavailable(format!(
"the identity table could not be read: {err}"
)))
}
}
}
pub fn require_repo_or_namespace(
&self,
caller: &Caller,
path: &gunnar_proto::RepoPath,
level: GrantLevel,
method: &'static str,
) -> Result<(), Status> {
let asked = Access::repo(path);
self.refuse_on_browse(caller, level, method)?;
if self.config.auth == AuthMode::AllowAllInsecure {
audit_allowed(
method,
caller,
&asked,
level,
self.config.auth,
self.ledger(),
);
return Ok(());
}
let unavailable = |err: crate::seam::ControlError| {
audit_denied(
method,
caller,
&asked,
level,
self.config.auth,
"grant lookup failed",
self.ledger(),
);
Status::unavailable(format!("the identity table could not be read: {err}"))
};
match self.control.may(&caller.principal_id, &asked, level) {
Ok(true) => {
audit_allowed(
method,
caller,
&asked,
level,
self.config.auth,
self.ledger(),
);
return Ok(());
}
Ok(false) => {}
Err(err) => return Err(unavailable(err)),
}
let wider = Access::namespace(&path.namespace);
match self.control.may(&caller.principal_id, &wider, level) {
Ok(true) => {
audit_allowed(
method,
caller,
&wider,
level,
self.config.auth,
self.ledger(),
);
Ok(())
}
Ok(false) => {
audit_denied(
method,
caller,
&asked,
level,
self.config.auth,
"no grant",
self.ledger(),
);
Err(Status::permission_denied(format!(
"principal {} has no {} grant on {asked}",
caller.principal_id,
crate::auth::level_name(level)
)))
}
Err(err) => Err(unavailable(err)),
}
}
pub fn require_or_narrow(
&self,
caller: &Caller,
access: Access,
method: &'static str,
) -> Result<Listing, Status> {
let level = GrantLevel::Read;
self.refuse_on_browse(caller, level, method)?;
if self.config.auth == AuthMode::AllowAllInsecure {
audit_allowed(
method,
caller,
&access,
level,
self.config.auth,
self.ledger(),
);
return Ok(Listing::Everything);
}
match self.control.may(&caller.principal_id, &access, level) {
Ok(true) => {
audit_allowed(
method,
caller,
&access,
level,
self.config.auth,
self.ledger(),
);
Ok(Listing::Everything)
}
Ok(false) => {
crate::auth::audit_narrowed(
method,
caller,
&access,
level,
self.config.auth,
self.ledger(),
);
Ok(Listing::PublicSubset)
}
Err(err) => {
audit_denied(
method,
caller,
&access,
level,
self.config.auth,
"grant lookup failed",
self.ledger(),
);
Err(Status::unavailable(format!(
"the identity table could not be read: {err}"
)))
}
}
}
pub fn retain_readable(
&self,
caller: &Caller,
repos: Vec<gunnar_proto::Repo>,
listing: Listing,
) -> Result<Vec<gunnar_proto::Repo>, Status> {
let mut readable = Vec::new();
let mut closed_ns: std::collections::HashMap<String, bool> = std::collections::HashMap::new();
let mut ask: Vec<(usize, gunnar_proto::RepoPath)> = Vec::new();
let mut slots: Vec<Option<gunnar_proto::Repo>> = Vec::new();
for repo in repos {
let Some(path) = repo.path.as_ref() else {
continue;
};
if listing == Listing::Everything {
let closed = match closed_ns.get(&path.namespace) {
Some(known) => *known,
None => {
let known = self.shielded(caller, &path.namespace)?;
closed_ns.insert(path.namespace.clone(), known);
known
}
};
if !closed {
slots.push(Some(repo));
continue;
}
}
ask.push((slots.len(), path.clone()));
slots.push(Some(repo));
}
if !ask.is_empty() {
let paths: Vec<gunnar_proto::RepoPath> = ask.iter().map(|(_, p)| p.clone()).collect();
let answers = self
.control
.may_each_repo(&caller.principal_id, &paths, GrantLevel::Read)
.map_err(|err| Status::unavailable(format!("the identity table could not be read: {err}")))?;
for ((slot, _), may) in ask.iter().zip(answers) {
if !may {
slots[*slot] = None;
}
}
}
readable.extend(slots.into_iter().flatten());
Ok(readable)
}
pub fn authorize<T>(
&self,
request: &Request<T>,
access: Access,
level: GrantLevel,
method: &'static str,
) -> Result<Caller, Status> {
let caller = self.caller(request)?;
self.require(&caller, access, level, method)?;
Ok(caller)
}
pub fn account_event(&self, account: Option<String>, event: crate::enterprise::AccountAuditEvent) {
let Some(account) = account else {
return;
};
let Some(plane) = self.control.enterprise() else {
return;
};
if let Err(err) = plane.account_audit_record(&account, &event) {
tracing::warn!(
target: crate::auth::AUDIT_TARGET,
event = "account_audit.not_written",
account = %account,
kind = %event.kind,
reason = %err,
"an event could not be written to its account's audit log"
);
}
}
pub fn enterprise(&self) -> Result<&dyn crate::enterprise::EnterprisePlane, Status> {
self.control.enterprise().ok_or_else(|| crate::enterprise::no_plane().into())
}
pub fn account_of_namespace(&self, namespace: &str) -> Option<String> {
self.control.enterprise()?.account_of_namespace(namespace).ok().flatten()
}
pub fn employer_of(&self, principal_id: &str) -> Result<Option<String>, Status> {
match self.control.employer_of(principal_id) {
Ok(employer) => Ok(employer),
Err(crate::seam::ControlError::Unimplemented(_)) => Ok(None),
Err(err) => Err(err.into()),
}
}
pub fn standing(&self, principal_id: &str) -> Result<Standing, Status> {
if let Some(account) = self.employer_of(principal_id)? {
let admin = self.display_name(&account)?;
let appointed = match self.control.may(principal_id, &Access::namespace(&account), gunnar_proto::GrantLevel::Admin) {
Ok(yes) => yes,
Err(crate::seam::ControlError::Unimplemented(_)) => false,
Err(err) => return Err(err.into()),
};
return Ok(Standing { account, role: if appointed { "admin-appointed" } else { "employee" }, admin });
}
let person = self
.control
.principal(principal_id)?
.is_some_and(|p| p.kind == gunnar_proto::PrincipalKind::User as i32);
if !person || self.control.namespace(principal_id)?.is_none() {
return Ok(Standing::default());
}
let (seats, _) = self.control.seats(principal_id)?;
let admin = seats > 0
|| match self.control.has_employees(principal_id) {
Ok(any) => any,
Err(crate::seam::ControlError::Unimplemented(_)) => false,
Err(err) => return Err(err.into()),
};
Ok(Standing {
account: principal_id.to_owned(),
role: if admin { "admin" } else { "individual" },
admin: self.display_name(principal_id)?,
})
}
fn display_name(&self, principal_id: &str) -> Result<String, Status> {
Ok(self
.control
.principal(principal_id)?
.map(|p| p.name)
.filter(|name| !name.trim().is_empty())
.unwrap_or_else(|| principal_id.to_owned()))
}
pub fn account_of_principal(&self, principal_id: &str) -> Option<String> {
self.control.enterprise()?.account_of_principal(principal_id).ok().flatten()
}
pub fn shielded(&self, caller: &Caller, namespace: &str) -> Result<bool, Status> {
if self.config.auth == AuthMode::AllowAllInsecure {
return Ok(false);
}
let Some(plane) = self.control.enterprise() else {
return Ok(false);
};
plane
.shielded(&caller.principal_id, namespace)
.map_err(|err| Status::unavailable(format!("the identity table could not be read: {err}")))
}
pub fn retain_unshielded<T>(
&self,
caller: &Caller,
rows: Vec<T>,
namespace_of: impl Fn(&T) -> Option<String>,
) -> Result<Vec<T>, Status> {
let mut kept = Vec::with_capacity(rows.len());
let mut closed_ns: std::collections::HashMap<String, bool> = std::collections::HashMap::new();
for row in rows {
let Some(ns) = namespace_of(&row) else {
kept.push(row);
continue;
};
let closed = match closed_ns.get(&ns) {
Some(known) => *known,
None => {
let known = self.shielded(caller, &ns)?;
closed_ns.insert(ns, known);
known
}
};
if !closed {
kept.push(row);
}
}
Ok(kept)
}
pub fn filter_name(
&self,
caller: &Caller,
kind: &str,
name: &str,
account: Option<String>,
exempt_reserved: bool,
) -> Result<(), Status> {
let Some(plane) = self.control.enterprise() else {
return Ok(());
};
let in_account = account.clone().unwrap_or_default();
let verdict = plane.name_check(kind, name, &in_account)?;
if verdict.is_clean() || (exempt_reserved && verdict.is_reserved()) {
return Ok(());
}
if verdict.is_borderline() && account.is_none() && exempt_reserved {
return Ok(());
}
let hold = verdict.is_borderline() && account.is_some();
let sentence = if verdict.is_borderline() && !hold {
format!(
"the {kind} name {name:?} needs a person's yes and there is no account admin to ask yet; \
choose another name"
)
} else {
verdict.sentence.clone()
};
if let Some(account) = account.as_deref() {
if let Err(err) = plane.name_review_add(kind, name, account, &caller.principal_id, hold) {
tracing::warn!(
target: crate::auth::AUDIT_TARGET,
event = "name_filter.not_queued",
account,
reason = %err,
"a name the filter stopped could not be put in its account's queue"
);
}
}
let kind_word = if hold { "name.held" } else { "name.refused" };
self.account_event(
account,
crate::enterprise::AccountAuditEvent::by(caller, kind_word, name)
.detail(format!("a {kind} name, {} by the name filter", verdict.verdict))
.refused(),
);
tracing::info!(
target: crate::auth::AUDIT_TARGET,
event = "name_filter.stopped",
principal = %caller.principal_id,
kind,
verdict = %verdict.verdict,
"the name filter stopped a name"
);
Err(if hold {
Status::failed_precondition(sentence)
} else {
Status::invalid_argument(sentence)
})
}
}
macro_rules! service {
($name:ident, $doc:literal) => {
#[doc = $doc]
#[derive(Clone)]
pub struct $name {
pub(crate) ctx: ServiceContext,
}
impl $name {
pub fn new(ctx: ServiceContext) -> Self {
Self { ctx }
}
}
impl From<ServiceContext> for $name {
fn from(ctx: ServiceContext) -> Self {
Self::new(ctx)
}
}
};
}
service!(ReposService, "Namespaces and repositories: the catalogue.");
service!(
AccountsService,
"The per-USER verbs: the account gate, and the hard purge that empties an \
account. `Repos` reclaims one repository at a time; this one takes a \
user's whole namespace, and locks them out while it does."
);
service!(
ClansService,
"Clans (2026-10-05): ordinary accounts grouped in a clan with a namespace \
of its own, each member giving it part of its own room; what is made in \
it is administered by the clan's admin."
);
service!(
RefsService,
"Refs, commit log, object reads, diffs. Read-only."
);
service!(AccessService, "Principals, public keys and grants (the ACL model).");
service!(
SsoService,
"Company single sign-on: an account's own identity provider (OIDC, SAML 2.0), \
SCIM 2.0 provisioning and the CSV import. Its people are EMPLOYEES of the \
account, created new or provisioned, never an attached principal."
);
service!(PolicyService, "Branch protection and ref-name policy (the pre-receive gate).");
service!(
EnterpriseService,
"The owner's enterprise list: the account admin's audit log, the operator's \
aggregates and the look the account admin grants, the name filter's lists \
and the account admin's review queue. Every account has it."
);
service!(
EventsService,
"`Watch`, the server-streaming event feed (the event stream)."
);
service!(
AdminService,
"Health, stats, storage backend info, and `Capacity` — \"could you serve a \
cap of N bytes?\", and how much storage the box has."
);
#[cfg(feature = "metered")]
service!(
EntitlementService,
"The paid state per namespace, written by monetize over `Set` and read by \
`authorise_service`. The METERED appliance's service: compiled only with \
the `metered` feature, and absent — not stubbed — without it."
);
#[cfg(feature = "metered")]
service!(
SealedService,
"A named sealed slot: bytes under a name, sealed under the KEK and logged \
as a `Secret` frame (the twin, snapshots and a recharge carry it), read \
and written only by the machine principal that owns it. The METERED \
appliance's, absent and not stubbed without the feature."
);
#[cfg(feature = "metered")]
service!(
BillingService,
"The tenant's own renewal path: the appliance's word, signed, that ONE \
human may act for ONE tenant — checked against the identity table here \
rather than by the console, which holds a server-wide grant. The METERED \
appliance's second service, absent and not stubbed without the feature; \
see `service::billing` for why the scope check cannot live upstream."
);
service!(
UsageService,
"Per-namespace meters (`pack_bytes`, `cache_bytes`, `lfs_bytes`, …): the \
numbers every storage report is made of, served to self-hosters and operators \
alike. Open source, always on."
);
service!(
UsageBatchService,
"Many namespaces' meters in one call, optionally only the ones that changed \
(`UsageBatch.ReadMany`, 2026-10-06): what a usage poller reads instead of \
one `Usage.Read` per namespace. Open source, always on."
);
service!(
ForgeService,
"Issues, change proposals and review, as signed append-only events \
(roadmap phase 1). The server never signs; it verifies and appends."
);
service!(MirrorService, "Pull-mirroring a remote forge (mirroring).");
service!(
WebhooksService,
"Outbound webhook delivery of event-stream events to external URLs, signed with an \
sshsig the receiver verifies against the server's published key."
);
service!(
PullsService,
"Pull requests: records in the control store, merges fast-forward-only \
through the store's own ref CAS."
);
service!(
BootstrapService,
"First-boot enrolment. **The one service served without a credential**, and \
only while nothing has ever been enrolled. It is mounted OUTSIDE \
[`crate::auth::AuthGate`]; see [`bootstrap`] for why that is safe and what \
holds it to two methods."
);
#[derive(Debug, Default)]
pub struct WebSessions {
minted: std::sync::Mutex<std::collections::HashMap<String, (bool, i64)>>,
}
impl WebSessions {
pub fn note(&self, fingerprint: &str, user_verified: bool, minted_unix_ms: i64) {
let mut minted = self.minted.lock().unwrap_or_else(|p| p.into_inner());
let floor = minted_unix_ms - nornir_identity_session::step_up::STEP_UP_WINDOW_SECS * 1000;
minted.retain(|_, (_, at)| *at >= floor);
minted.insert(fingerprint.to_owned(), (user_verified, minted_unix_ms));
}
pub fn stepped_up(&self, fingerprint: &str, now_unix_ms: i64) -> bool {
let minted = self.minted.lock().unwrap_or_else(|p| p.into_inner());
minted.get(fingerprint).is_some_and(|(uv, at)| {
nornir_identity_session::step_up::stepped_up(*uv, at.div_euclid(1000), now_unix_ms.div_euclid(1000))
})
}
}
macro_rules! presented {
($up:expr) => {
nornir_identity_session::step_up::Presented {
credential_id: &$up.credential_id,
client_data_json: &$up.client_data_json,
authenticator_data: &$up.authenticator_data,
signature: &$up.signature,
rp_id: &$up.rp_id,
origin: &$up.origin,
}
};
}
pub(crate) use presented;
pub(crate) fn step_up_of_own_passkey(
ctx: &ServiceContext,
up: nornir_identity_session::step_up::Presented<'_>,
caller: &str,
) -> Result<Result<nornir_identity_session::step_up::SteppedUp, nornir_identity_session::step_up::Refused>, Status> {
nornir_identity_session::step_up::verify_own_passkey(
&up,
caller,
&ctx.web_challenges,
&ctx.config.web_relying_parties,
crate::bus::now_unix_ms(),
|id| {
Ok::<_, Status>(ctx.control.credential(id)?.map(|stored| nornir_identity_session::step_up::Stored {
principal_id: stored.credential.principal_id,
public_key: stored.public_key,
sign_count: stored.credential.sign_count,
}))
},
)
}
#[cfg(test)]
mod narrowed_tests {
use super::*;
use crate::seam::MemoryControlPlane;
fn caller(id: &str) -> Caller {
Caller { principal_id: id.into(), fingerprint: "SHA256:x".into(), machine: true, authenticated: true }
}
#[test]
fn a_narrowed_principal_may_call_its_list_and_nothing_else() {
let plane = Arc::new(MemoryControlPlane::new());
plane.narrow("monetize-central", crate::BILLING_METHODS);
let ctx = ServiceContext::new(plane.clone(), Arc::new(crate::bus::EventBus::new(8)), crate::ServerConfig::default());
let c = caller("monetize-central");
for m in ["Entitlement.Set", "Admin.Capacity", "Admin.GrowStart", "Usage.ReadAll"] {
let m: &'static str = crate::BILLING_METHODS.iter().copied().find(|x| *x == m).expect("on the list");
ctx.require(&c, Access::Server, GrantLevel::Admin, m).unwrap_or_else(|e| panic!("{m}: {e:?}"));
}
for m in ["Admin.SetTunnelAddress", "Access.AddKey", "Mirror.SetRole", "Admin.IssueTwinCertificate"] {
let e = ctx.require(&c, Access::Server, GrantLevel::Admin, m).unwrap_err();
assert_eq!(e.code(), tonic::Code::PermissionDenied, "{m}");
assert!(e.message().contains("narrowed"), "{m}: {}", e.message());
}
crate::seam::ControlPlane::add_principal(plane.as_ref(), "rickard", "rickard", gunnar_proto::PrincipalKind::User).unwrap();
plane.grant_server("rickard", GrantLevel::Admin);
ctx.require(&caller("rickard"), Access::Server, GrantLevel::Admin, "Admin.SetTunnelAddress").expect("an admin is an admin");
}
}