Skip to main content

lean_ctx/core/trust/
identity.rs

1//! Stable identity value types shared by local and enterprise deployments.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6macro_rules! identity_id {
7    ($name:ident, $documentation:literal) => {
8        #[doc = $documentation]
9        #[derive(
10            Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
11        )]
12        #[serde(transparent)]
13        pub struct $name(String);
14
15        impl $name {
16            /// Creates an identifier from its opaque string representation.
17            #[must_use]
18            pub fn new(value: impl Into<String>) -> Self {
19                Self(value.into())
20            }
21
22            /// Returns the opaque identifier without allocating.
23            #[must_use]
24            pub fn as_str(&self) -> &str {
25                &self.0
26            }
27
28            /// Returns whether the identifier is empty.
29            #[must_use]
30            pub fn is_empty(&self) -> bool {
31                self.0.is_empty()
32            }
33        }
34
35        impl From<&str> for $name {
36            fn from(value: &str) -> Self {
37                Self::new(value)
38            }
39        }
40
41        impl From<String> for $name {
42            fn from(value: String) -> Self {
43                Self::new(value)
44            }
45        }
46
47        impl AsRef<str> for $name {
48            fn as_ref(&self) -> &str {
49                self.as_str()
50            }
51        }
52
53        impl fmt::Display for $name {
54            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55                formatter.write_str(self.as_str())
56            }
57        }
58    };
59}
60
61identity_id!(
62    TenantId,
63    "Opaque identifier for the tenant that owns an execution or data object."
64);
65identity_id!(
66    ProjectId,
67    "Opaque identifier for a project within a tenant."
68);
69identity_id!(
70    AgentId,
71    "Opaque identifier for an agent execution identity."
72);
73identity_id!(
74    HumanActorId,
75    "Opaque identifier for the human actor responsible for an action."
76);
77identity_id!(
78    ServiceId,
79    "Opaque identifier for a service acting on behalf of a tenant."
80);
81
82/// Scope at which an identity or reference is valid.
83#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
84pub enum IdentityScope {
85    /// Valid only inside one local runtime or process.
86    #[default]
87    Local,
88    /// Valid for one tenant and its authorized projects.
89    Tenant,
90    /// Explicitly spans more than one tenant and requires enterprise policy.
91    CrossTenant,
92}
93
94/// Identity attributes carried with a request, evidence entry, or policy
95/// evaluation.
96#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
97pub struct IdentityContext {
98    /// Owning tenant. This is mandatory even for local contexts.
99    pub tenant: TenantId,
100    /// Optional project within the tenant.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub project: Option<ProjectId>,
103    /// Optional agent execution identity.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub agent: Option<AgentId>,
106    /// Optional human actor who initiated or approved the action.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub human_actor: Option<HumanActorId>,
109    /// Optional service identity used for a delegated operation.
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub service: Option<ServiceId>,
112    /// Optional session or trace-local identity.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub session: Option<String>,
115}
116
117impl IdentityContext {
118    /// Creates a context with only its required tenant identity.
119    #[must_use]
120    pub fn new(tenant: impl Into<TenantId>) -> Self {
121        Self {
122            tenant: tenant.into(),
123            project: None,
124            agent: None,
125            human_actor: None,
126            service: None,
127            session: None,
128        }
129    }
130
131    /// Sets the project scope.
132    #[must_use]
133    pub fn with_project(mut self, project: impl Into<ProjectId>) -> Self {
134        self.project = Some(project.into());
135        self
136    }
137
138    /// Sets the agent identity.
139    #[must_use]
140    pub fn with_agent(mut self, agent: impl Into<AgentId>) -> Self {
141        self.agent = Some(agent.into());
142        self
143    }
144
145    /// Sets the human actor identity.
146    #[must_use]
147    pub fn with_human_actor(mut self, human_actor: impl Into<HumanActorId>) -> Self {
148        self.human_actor = Some(human_actor.into());
149        self
150    }
151
152    /// Sets the delegated service identity.
153    #[must_use]
154    pub fn with_service(mut self, service: impl Into<ServiceId>) -> Self {
155        self.service = Some(service.into());
156        self
157    }
158
159    /// Sets the session identity.
160    #[must_use]
161    pub fn with_session(mut self, session: impl Into<String>) -> Self {
162        self.session = Some(session.into());
163        self
164    }
165
166    /// Returns the tenant without exposing mutable identity state.
167    #[must_use]
168    pub fn tenant_id(&self) -> &TenantId {
169        &self.tenant
170    }
171
172    /// Returns the most specific declared scope for this context.
173    #[must_use]
174    pub fn scope(&self) -> IdentityScope {
175        if self.tenant.is_empty() {
176            IdentityScope::Local
177        } else {
178            IdentityScope::Tenant
179        }
180    }
181}