Skip to main content

mkit_server/
principal.rs

1//! Principals: who a request acts as (PRD §5.4 stage 1, identity mapping).
2
3/// The identity a request was mapped to. Non-exhaustive: M2 may add a
4/// caller-view class.
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6#[non_exhaustive]
7pub enum Principal {
8    /// No credentials.
9    Anonymous,
10    /// An auth v2 signer.
11    Signer {
12        /// The signer's raw Ed25519 public key.
13        ed25519: [u8; 32],
14    },
15    /// Holder of a deployment-wide shared bearer token (`mkit-server
16    /// serve --auth bearer`, formerly `mkit serve --http`).
17    BearerHolder,
18    /// The authenticated peer of an encrypted (`enc`) listener.
19    TransportPeer {
20        /// The peer's raw Ed25519 static key.
21        ed25519: [u8; 32],
22    },
23    /// `mkit serve` over stdio. The identity is the ssh forced command
24    /// (SSH-SECURITY §5). `key` is always `None` in M0; WP-1.15 sets it from
25    /// `mkit serve --principal <ed25519-hex>`.
26    SshForcedCommand {
27        /// The Ed25519 key the forced command names, if any.
28        key: Option<[u8; 32]>,
29    },
30}
31
32impl Principal {
33    /// A stable, key-free label for tracing spans and metrics:
34    /// `anonymous`, `signer`, `bearer`, `transport_peer` or
35    /// `ssh_forced_command`.
36    #[must_use]
37    pub const fn kind(&self) -> &'static str {
38        match self {
39            Self::Anonymous => "anonymous",
40            Self::Signer { .. } => "signer",
41            Self::BearerHolder => "bearer",
42            Self::TransportPeer { .. } => "transport_peer",
43            Self::SshForcedCommand { .. } => "ssh_forced_command",
44        }
45    }
46
47    /// The principal's Ed25519 public key, when it has one.
48    #[must_use]
49    pub fn ed25519(&self) -> Option<&[u8; 32]> {
50        match self {
51            Self::Signer { ed25519 } | Self::TransportPeer { ed25519 } => Some(ed25519),
52            Self::SshForcedCommand { key } => key.as_ref(),
53            Self::Anonymous | Self::BearerHolder => None,
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn ed25519_accessor_covers_ssh_key_some_and_none() {
64        let key = [7u8; 32];
65        assert_eq!(Principal::Signer { ed25519: key }.ed25519(), Some(&key));
66        assert_eq!(
67            Principal::TransportPeer { ed25519: key }.ed25519(),
68            Some(&key)
69        );
70        assert_eq!(
71            Principal::SshForcedCommand { key: Some(key) }.ed25519(),
72            Some(&key)
73        );
74        assert_eq!(Principal::SshForcedCommand { key: None }.ed25519(), None);
75        assert_eq!(Principal::Anonymous.ed25519(), None);
76        assert_eq!(Principal::BearerHolder.ed25519(), None);
77    }
78
79    #[test]
80    fn kind_labels_carry_no_key_material() {
81        let key = [7u8; 32];
82        let cases = [
83            (Principal::Anonymous, "anonymous"),
84            (Principal::Signer { ed25519: key }, "signer"),
85            (Principal::BearerHolder, "bearer"),
86            (Principal::TransportPeer { ed25519: key }, "transport_peer"),
87            (
88                Principal::SshForcedCommand { key: Some(key) },
89                "ssh_forced_command",
90            ),
91        ];
92        for (principal, kind) in cases {
93            assert_eq!(principal.kind(), kind);
94        }
95    }
96}