greentic_types/
tenant.rs

1//! Tenant-centric identity helpers.
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5
6#[cfg(feature = "schemars")]
7use schemars::JsonSchema;
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11use crate::{TeamId, TenantContext, TenantCtx, TenantId, UserId};
12
13/// Metadata describing an impersonated user acting on behalf of the main identity.
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16#[cfg_attr(feature = "schemars", derive(JsonSchema))]
17pub struct Impersonation {
18    /// Identifier of the user performing the impersonation.
19    pub actor_id: UserId,
20    /// Optional justification recorded for auditing.
21    #[cfg_attr(
22        feature = "serde",
23        serde(default, skip_serializing_if = "Option::is_none")
24    )]
25    pub reason: Option<String>,
26}
27
28/// Stable multi-tenant identity extracted from [`TenantCtx`].
29#[derive(Clone, Debug, PartialEq, Eq, Hash)]
30#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
31#[cfg_attr(feature = "schemars", derive(JsonSchema))]
32pub struct TenantIdentity {
33    /// Tenant identifier.
34    pub tenant_id: TenantId,
35    /// Optional team identifier.
36    #[cfg_attr(
37        feature = "serde",
38        serde(default, skip_serializing_if = "Option::is_none")
39    )]
40    pub team_id: Option<TeamId>,
41    /// Optional user identifier.
42    #[cfg_attr(
43        feature = "serde",
44        serde(default, skip_serializing_if = "Option::is_none")
45    )]
46    pub user_id: Option<UserId>,
47    /// Optional impersonation information.
48    #[cfg_attr(
49        feature = "serde",
50        serde(default, skip_serializing_if = "Option::is_none")
51    )]
52    pub impersonation: Option<Impersonation>,
53    /// Free-form attributes propagated for routing and tracing.
54    #[cfg_attr(
55        feature = "serde",
56        serde(default, skip_serializing_if = "BTreeMap::is_empty")
57    )]
58    pub attributes: BTreeMap<String, String>,
59}
60
61impl TenantIdentity {
62    /// Creates a new tenant identity scoped to a tenant id.
63    pub fn new(tenant_id: TenantId) -> Self {
64        Self {
65            tenant_id,
66            team_id: None,
67            user_id: None,
68            impersonation: None,
69            attributes: BTreeMap::new(),
70        }
71    }
72}
73
74impl From<&TenantCtx> for TenantIdentity {
75    fn from(ctx: &TenantCtx) -> Self {
76        Self {
77            tenant_id: ctx.tenant_id.clone(),
78            team_id: ctx.team_id.clone().or_else(|| ctx.team.clone()),
79            user_id: ctx.user_id.clone().or_else(|| ctx.user.clone()),
80            impersonation: ctx.impersonation.clone(),
81            attributes: ctx.attributes.clone(),
82        }
83    }
84}
85
86impl TenantCtx {
87    /// Returns the tenant identity derived from this context.
88    pub fn identity(&self) -> TenantIdentity {
89        TenantIdentity::from(self)
90    }
91
92    /// Returns the lightweight tenant context shared with tooling.
93    pub fn tenant_context(&self) -> TenantContext {
94        TenantContext::from(self)
95    }
96
97    /// Returns the impersonation context, when present.
98    pub fn impersonated_by(&self) -> Option<&Impersonation> {
99        self.impersonation.as_ref()
100    }
101
102    /// Updates the identity fields to match the provided value.
103    pub fn with_identity(mut self, identity: TenantIdentity) -> Self {
104        self.tenant = identity.tenant_id.clone();
105        self.tenant_id = identity.tenant_id;
106        self.team = identity.team_id.clone();
107        self.team_id = identity.team_id;
108        self.user = identity.user_id.clone();
109        self.user_id = identity.user_id;
110        self.impersonation = identity.impersonation;
111        self.attributes = identity.attributes;
112        self
113    }
114}