rightkit_daemon/contract/caller.rs
1use crate::peer::PeerIdentity;
2use std::collections::BTreeSet;
3
4/// A capability a device holds (from enrolment). A plain label compared for equality.
5pub type Scope = String;
6
7/// A scope that names a role instead of a capability: `role:<name>`, for example `role:operator`.
8///
9/// A device's role is whatever its enrolment granted. Scopes are stored with the device record
10/// when it enrols (by an enrolment code or a pairing window the owner issued) and read back after
11/// the request's signature verified, so a role taken from [`Caller::role`] is the role of the
12/// signing device: no environment variable, header or request field can set it. Gate a method on
13/// a role by naming the role scope in its [`MethodPolicy`](crate::contract::MethodPolicy):
14/// `MethodPolicy::remote(["role:operator"])`.
15pub const ROLE_SCOPE_PREFIX: &str = "role:";
16
17/// The scope that grants `role`.
18pub fn role_scope(role: &str) -> Scope {
19 format!("{ROLE_SCOPE_PREFIX}{role}")
20}
21
22/// The one role among `scopes`: `Some` only when exactly one scope is a role scope with a
23/// non-empty name. Two roles, or none, give `None`, so an ambiguous device never gets a role by
24/// the luck of an iteration order.
25pub fn role_of<'a>(scopes: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
26 let mut found = None;
27 for scope in scopes {
28 if let Some(name) = scope.strip_prefix(ROLE_SCOPE_PREFIX) {
29 if name.is_empty() || found.is_some() {
30 return None;
31 }
32 found = Some(name);
33 }
34 }
35 found
36}
37
38/// Who is making a request.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Caller {
41 /// A local app on this machine. `app` is the label declared in `hello` (a claim, an allowlist
42 /// key); `peer` is what the OS verified about the other end.
43 LocalApp { app: String, peer: PeerIdentity },
44 /// A remote device identified by its id, with the scopes it was enrolled with.
45 Device { id: String, scopes: BTreeSet<Scope> },
46}
47
48impl Caller {
49 pub fn local_app(app: impl Into<String>, peer: PeerIdentity) -> Self {
50 Caller::LocalApp {
51 app: app.into(),
52 peer,
53 }
54 }
55
56 pub fn device<I, S>(id: impl Into<String>, scopes: I) -> Self
57 where
58 I: IntoIterator<Item = S>,
59 S: Into<Scope>,
60 {
61 Caller::Device {
62 id: id.into(),
63 scopes: scopes.into_iter().map(Into::into).collect(),
64 }
65 }
66
67 /// The OS-verified peer, for a local caller only.
68 pub fn peer(&self) -> Option<&PeerIdentity> {
69 match self {
70 Caller::LocalApp { peer, .. } => Some(peer),
71 Caller::Device { .. } => None,
72 }
73 }
74
75 /// The role of a device caller (see [`ROLE_SCOPE_PREFIX`]); `None` for a local app, for a
76 /// device with no role scope and for one with more than one.
77 pub fn role(&self) -> Option<&str> {
78 match self {
79 Caller::LocalApp { .. } => None,
80 Caller::Device { scopes, .. } => role_of(scopes.iter().map(String::as_str)),
81 }
82 }
83
84 /// The app label for a local caller, the device id for a device.
85 pub fn label(&self) -> &str {
86 match self {
87 Caller::LocalApp { app, .. } => app,
88 Caller::Device { id, .. } => id,
89 }
90 }
91}