apiplant_server/access.rs
1//! Answering "may this caller do this", in the one grammar the whole framework
2//! uses.
3//!
4//! A resource's `[permissions]`, a function's manifest and `[ai] access` all
5//! spell access the same way — `public`, `authenticated`, `member`,
6//! `role:<name>`, `private` — so the check that enforces it lives here rather
7//! than once per endpoint. Two callers today ([functions](crate::function_routes)
8//! and [the assistant](crate::ai_routes)); the value is that neither can drift
9//! from the other.
10
11use apiplant_abi::FunctionAccess;
12use apiplant_auth::Principal;
13use ntex::web::types::State;
14use ntex::web::{HttpRequest, HttpResponse};
15
16use crate::response::error;
17use crate::state::AppState;
18
19/// Resolve the caller and check them against `access`.
20///
21/// `Ok` carries the principal — `None` for an anonymous caller of a `public`
22/// endpoint. `Err` is the response to send, already the right status: `401`
23/// when credentials would help, `403` when they wouldn't, and `404` for
24/// `private`, which is not merely forbidden but not there.
25pub async fn check(
26 state: &State<AppState>,
27 req: &HttpRequest,
28 access: &FunctionAccess,
29 missing: &str,
30) -> Result<Option<Principal>, HttpResponse> {
31 let principal = state.resolve_principal(req).await;
32
33 match access {
34 FunctionAccess::Public => {}
35 // Not "you may not", but "there is nothing here" — so probing cannot
36 // enumerate what exists.
37 FunctionAccess::Private => return Err(error(404, missing.to_string())),
38 FunctionAccess::Authenticated => {
39 if principal.is_none() {
40 return Err(error(401, "authentication required"));
41 }
42 }
43 // `member` and `role:` are both organisation-scoped: they need an
44 // active organisation the caller actually belongs to.
45 FunctionAccess::Member | FunctionAccess::Role(_) => {
46 if principal.is_none() {
47 return Err(error(401, "authentication required"));
48 }
49 // `active_org` already refuses an organisation the caller does not
50 // belong to, so membership is settled by having one at all — a
51 // member with no role is still a member.
52 let org = state.active_org(req, &principal);
53 let ok = match (access, org, principal.as_ref()) {
54 (FunctionAccess::Member, Some(org), Some(caller)) => caller.is_member(org),
55 (FunctionAccess::Role(required), Some(org), Some(caller)) => {
56 // Any of the caller's roles will do, and an admin holds all.
57 caller.has_role_in(org, required)
58 }
59 _ => false,
60 };
61 if !ok {
62 return Err(error(403, "forbidden"));
63 }
64 }
65 }
66 Ok(principal)
67}