greentic_types/
tenant.rs

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