Skip to main content

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}