aauth_person_server/lib.rs
1//! An AAuth Person Server ([draft-hardt-oauth-aauth-protocol] `-11`,
2//! three-party) you can run instead of building one.
3//!
4//! A Person Server speaks for people when agents want to act for them. This
5//! one:
6//!
7//! - verifies each agent by its HTTP Message Signature and the agent token
8//! it presents, from agent providers you trust;
9//! - puts each **mission** an agent proposes to whoever decides for the
10//! person, and issues **person tokens** and **auth tokens** under it;
11//! - weighs each auth token request against its mission's **budget**
12//! ([draft-hardt-aauth-budgets]): one that fits what is left is issued at
13//! once and spent, and one that does not goes back to the approver;
14//! - turns whatever needs a human into an [`Approval`], which you deliver
15//! however you reach people, and answer with [`PersonServer::decide`].
16//!
17//! It is a port of [`@introspection-sdk/person-server`]. The two speak the
18//! same protocol, and an agent cannot tell them apart.
19//!
20//! # Running one
21//!
22//! You supply a [`Directory`]: who you speak for, by the `login_hint` an
23//! agent names, and who decides for them. [`Hooks`] tell you when a decision
24//! is needed. [`PersonServer::handle`] takes an `http::Request<Bytes>` and
25//! returns an `http::Response<Bytes>`, so it mounts behind hyper, axum, or
26//! anything else built on the `http` crate; `examples/server.rs` runs it on
27//! hyper.
28//!
29//! ```
30//! use aauth_person_server::{
31//! Approval, ApprovalKind, Config, Decision, Directory, Hooks, Person, PersonServer,
32//! };
33//! use async_trait::async_trait;
34//! use bytes::Bytes;
35//!
36//! struct Staff;
37//!
38//! #[async_trait]
39//! impl Directory for Staff {
40//! async fn person(&self, login_hint: &str, _tenant: &str) -> Option<Person> {
41//! let email = login_hint.strip_prefix("user:")?.strip_suffix("@acme.example")?;
42//! Some(Person::new(email))
43//! }
44//!
45//! async fn approver(&self, kind: ApprovalKind, _person: &Person) -> Option<Person> {
46//! // Managers approve trips; completing one is the traveller's own call.
47//! (kind != ApprovalKind::Completion).then(|| Person::new("dana"))
48//! }
49//! }
50//!
51//! struct Notify;
52//!
53//! #[async_trait]
54//! impl Hooks for Notify {
55//! async fn on_approval(&self, approval: &Approval) {
56//! println!("ask {} about {}", approval.approver.id, approval.description);
57//! }
58//! }
59//!
60//! # #[tokio::main(flavor = "current_thread")]
61//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
62//! let ps = PersonServer::new(
63//! Config::new("https://ps.acme.example", Staff, ["https://api.introspection.dev"])
64//! // In production, a stable key and secret; unset, both are fresh per process.
65//! .signing_key(aauth_protocol::SigningKey::generate())
66//! .subject_secret("a long random secret")
67//! .hooks(Notify),
68//! )?;
69//!
70//! let request = http::Request::get("/.well-known/aauth-person.json").body(Bytes::new())?;
71//! let response = ps.handle(request).await;
72//! assert_eq!(response.status(), http::StatusCode::OK);
73//!
74//! // Later, when Dana answers an approval:
75//! # let approval_id = "an-approval-id";
76//! # let _ =
77//! ps.decide("default", approval_id, Decision::approve().by("dana@acme.example")).await;
78//! # Ok(())
79//! # }
80//! ```
81//!
82//! # Policy
83//!
84//! - A mission is always put to the approver, who may set its budget.
85//! - An auth token needs a live mission covering the resource token's
86//! issuer, and only those issuers' keys are ever fetched.
87//! - A request whose `budget` is in the mission's unit and scale, and fits
88//! what is left, is issued at once and spent. Any other budgeted request
89//! is put to the approver, and spent when approved.
90//! - A request with no `budget` is issued within the mission without spending.
91//! - Completing a mission is the person's own decision; nothing is issued
92//! under it afterwards.
93//! - Each agent key is bound to the first person it names.
94//!
95//! # Routes
96//!
97//! | Route | |
98//! | --- | --- |
99//! | `GET /.well-known/aauth-person.json` | metadata |
100//! | `GET /.well-known/jwks.json` | token-signing keys |
101//! | `POST /mission` | propose a mission |
102//! | `POST /mission/{s256}` | `update` or `completion` |
103//! | `POST /person-token` | a person token |
104//! | `POST /token` | an auth token, for a resource token |
105//! | `GET`, `DELETE /pending/{tenant}/{id}` | poll, or withdraw, a pending request |
106//!
107//! With [`Config::admin_token`], the management routes are on too, behind
108//! `Authorization: Bearer <token>`: `GET /consent[?status=]`,
109//! `POST /consent/{id}/decision`, `GET /missions[/{s256}]`, and
110//! `PATCH /missions/{s256}` to terminate one; each takes `?tenant=`.
111//!
112//! # Tenants
113//!
114//! A Person Server can serve many organisations at once. An agent names its
115//! AAuth `tenant` in each request body, and a request that names none is
116//! under the server's default tenant ([`Config::default_tenant`],
117//! [`DEFAULT_TENANT`] unless set; [`Config::require_tenant`] refuses such a
118//! request instead). The pending URL carries the tenant, tokens carry the
119//! one the agent named or the directory gave, and every [`Store`] call is
120//! scoped by it, so one tenant's missions, approvals and key bindings are
121//! never another's. A person the directory places in a tenant is in that
122//! tenant alone.
123//!
124//! # Deploying
125//!
126//! - [`MemoryStore`] is one process's memory, and keeps missions and key
127//! bindings forever. For more than one process, implement [`Store`] on a
128//! database: its `spend`, `transition_approval` and `insert_mission` must
129//! be atomic, which is what makes each budget spend and each decision
130//! happen once, and keeps one mission from replacing another. A store
131//! that fails answers with a [`StoreError`], which the agent sees as a
132//! `503`.
133//! - Bound request bodies before handing them over (the example caps them
134//! at 256 KiB), and rate-limit in front: this crate does neither.
135//! - Only a trusted agent provider's keys, a mission's resources' keys, and
136//! a resource's own R3 documents are ever fetched, through
137//! `ReqwestFetch::guarded` (the `reqwest` feature, on by default): `https`
138//! only, never an IP literal, `localhost` or a private address. Give
139//! [`Config::fetch`] your own to change that.
140//! - An approved auth token is collected once: the poll that receives it
141//! moves the approval to [`ApprovalStatus::Collected`], and later polls
142//! are `410 Gone`.
143//!
144//! [draft-hardt-oauth-aauth-protocol]: https://github.com/dickhardt/AAuth
145//! [draft-hardt-aauth-budgets]: https://github.com/dickhardt/AAuth
146//! [`@introspection-sdk/person-server`]: https://www.npmjs.com/package/@introspection-sdk/person-server
147
148#![deny(missing_docs)]
149#![forbid(unsafe_code)]
150#![cfg_attr(docsrs, feature(doc_cfg))]
151
152mod directory;
153mod error;
154mod server;
155mod store;
156mod time;
157mod types;
158
159pub use aauth_protocol;
160pub use directory::{Directory, Hooks, NoHooks};
161pub use error::{problem, AAuthError};
162pub use server::{Config, Endpoints, PersonServer, DEFAULT_TENANT};
163pub use store::{MemoryStore, Spend, Store, StoreError, StoreResult};
164pub use types::{
165 Agent, Approval, ApprovalKind, ApprovalStatus, Decision, Issued, IssuedKind, LogEntry, Mission,
166 Person, Verdict,
167};