Skip to main content

everruns_core/
organization.rs

1// Organization types for multitenancy
2// See specs/multitenancy.md
3//
4// Decision: Hierarchical org roles (Owner > Admin > Member) using PartialOrd.
5// External auth providers map their roles to OrgRole via AuthBackend trait.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::str::FromStr;
11use uuid::Uuid;
12
13/// Default organization ID (internal, for DB queries)
14pub const DEFAULT_ORG_ID: i64 = 1;
15
16/// Default organization public ID (external, for API)
17pub const DEFAULT_ORG_PUBLIC_ID: &str = "org_00000000000000000000000000000001";
18
19/// Well-known anonymous user UUID for auth=none mode.
20/// This is a real database user seeded at startup, so all code paths
21/// (org membership, API keys, etc.) work without special-casing.
22pub const ANONYMOUS_USER_ID: Uuid = Uuid::from_bytes([
23    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
24]);
25
26/// Anonymous user email
27pub const ANONYMOUS_USER_EMAIL: &str = "anonymous@local";
28
29/// Anonymous user display name
30pub const ANONYMOUS_USER_NAME: &str = "Anonymous";
31
32/// Organization entity (domain type)
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
35pub struct Organization {
36    /// External identifier (org_<32-hex-chars>)
37    pub public_id: String,
38    /// Display name
39    pub name: String,
40    pub created_at: DateTime<Utc>,
41    pub updated_at: DateTime<Utc>,
42}
43
44/// Organization-level role with hierarchical permissions.
45/// Owner > Admin > Member — checked via `has_permission()`.
46#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
48#[serde(rename_all = "lowercase")]
49pub enum OrgRole {
50    Member,
51    Admin,
52    #[default]
53    Owner,
54}
55
56impl OrgRole {
57    /// Check if this role has at least the `required` permission level.
58    pub fn has_permission(self, required: OrgRole) -> bool {
59        self.level() >= required.level()
60    }
61
62    /// String representation for DB storage.
63    pub fn as_str(self) -> &'static str {
64        match self {
65            OrgRole::Member => "member",
66            OrgRole::Admin => "admin",
67            OrgRole::Owner => "owner",
68        }
69    }
70
71    fn level(self) -> u8 {
72        match self {
73            OrgRole::Member => 0,
74            OrgRole::Admin => 1,
75            OrgRole::Owner => 2,
76        }
77    }
78}
79
80impl fmt::Display for OrgRole {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        f.write_str(self.as_str())
83    }
84}
85
86impl FromStr for OrgRole {
87    type Err = String;
88
89    fn from_str(s: &str) -> Result<Self, Self::Err> {
90        match s {
91            "member" => Ok(OrgRole::Member),
92            "admin" => Ok(OrgRole::Admin),
93            "owner" => Ok(OrgRole::Owner),
94            _ => Err(format!("invalid org role: {s}")),
95        }
96    }
97}
98
99/// Organization membership info (for user context)
100#[derive(Debug, Clone, Serialize, Deserialize)]
101#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
102pub struct OrgMembership {
103    /// Internal org_id for DB queries (not serialized to API)
104    #[serde(skip_serializing)]
105    pub org_id: i64,
106    /// External identifier
107    pub public_id: String,
108    /// Display name
109    pub name: String,
110    /// User's role in this organization
111    #[serde(default)]
112    pub role: OrgRole,
113}
114
115/// Derive a deterministic public_id from an internal org_id.
116///
117/// For `DEFAULT_ORG_ID` this returns `DEFAULT_ORG_PUBLIC_ID`.
118/// For other IDs it produces `org_<032x>` so callers can avoid
119/// an async DB lookup when only the public_id format is needed.
120pub fn org_public_id_from_internal(org_id: i64) -> String {
121    if org_id == DEFAULT_ORG_ID {
122        return DEFAULT_ORG_PUBLIC_ID.to_string();
123    }
124    format!("org_{:032x}", org_id)
125}
126
127/// Recover the internal `org_id` from an [`OrgId`] derived via
128/// [`org_public_id_from_internal`].
129///
130/// `org_public_id_from_internal` encodes the internal `i64` as the low bits of
131/// the public id's 32-hex payload, which round-trips through the `OrgId` UUID.
132/// Used where a tool only has the public [`OrgId`] but a store needs the
133/// internal id (e.g. `search_index` → `KnowledgeIndexSearch`, `search_knowledge`
134/// → `KnowledgeStore`).
135pub fn org_internal_id_from_public(org_id: crate::typed_id::OrgId) -> i64 {
136    org_id.uuid().as_u128() as i64
137}
138
139/// Generate a new organization public ID
140/// Format: org_<32-hex-chars> (UUIDv4 lowercase hex, no dashes)
141pub fn generate_org_public_id() -> String {
142    let uuid = Uuid::new_v4();
143    format!("org_{}", uuid.simple())
144}
145
146/// Validate organization public ID format
147/// Pattern: ^org_[0-9a-f]{32}$
148pub fn validate_org_public_id(public_id: &str) -> bool {
149    if !public_id.starts_with("org_") {
150        return false;
151    }
152    let suffix = &public_id[4..];
153    suffix.len() == 32
154        && suffix
155            .chars()
156            .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_generate_org_public_id() {
165        let id = generate_org_public_id();
166        assert!(id.starts_with("org_"));
167        assert_eq!(id.len(), 36); // "org_" + 32 hex chars
168        assert!(validate_org_public_id(&id));
169    }
170
171    #[test]
172    fn test_validate_org_public_id() {
173        // Valid
174        assert!(validate_org_public_id(
175            "org_00000000000000000000000000000001"
176        ));
177        assert!(validate_org_public_id(
178            "org_2f3c1b3e6a9d4c6f8a1d4e9c9b7f21a0"
179        ));
180
181        // Invalid - wrong prefix
182        assert!(!validate_org_public_id(
183            "organization_12345678901234567890123456789012"
184        ));
185
186        // Invalid - too short
187        assert!(!validate_org_public_id("org_123"));
188
189        // Invalid - too long
190        assert!(!validate_org_public_id(
191            "org_123456789012345678901234567890123"
192        ));
193
194        // Invalid - uppercase
195        assert!(!validate_org_public_id(
196            "org_2F3C1B3E6A9D4C6F8A1D4E9C9B7F21A0"
197        ));
198
199        // Invalid - non-hex characters
200        assert!(!validate_org_public_id(
201            "org_ghijklmnopqrstuvwxyz1234567890"
202        ));
203    }
204
205    #[test]
206    fn test_default_org_public_id_valid() {
207        assert!(validate_org_public_id(DEFAULT_ORG_PUBLIC_ID));
208    }
209
210    #[test]
211    fn test_org_internal_id_round_trips() {
212        for internal in [DEFAULT_ORG_ID, 5, 42, 1_000_000] {
213            let public = org_public_id_from_internal(internal);
214            let org_id: crate::typed_id::OrgId = public.parse().expect("parse org id");
215            assert_eq!(org_internal_id_from_public(org_id), internal);
216        }
217    }
218
219    #[test]
220    fn test_org_role_hierarchy() {
221        assert!(OrgRole::Owner.has_permission(OrgRole::Owner));
222        assert!(OrgRole::Owner.has_permission(OrgRole::Admin));
223        assert!(OrgRole::Owner.has_permission(OrgRole::Member));
224
225        assert!(!OrgRole::Admin.has_permission(OrgRole::Owner));
226        assert!(OrgRole::Admin.has_permission(OrgRole::Admin));
227        assert!(OrgRole::Admin.has_permission(OrgRole::Member));
228
229        assert!(!OrgRole::Member.has_permission(OrgRole::Owner));
230        assert!(!OrgRole::Member.has_permission(OrgRole::Admin));
231        assert!(OrgRole::Member.has_permission(OrgRole::Member));
232    }
233
234    #[test]
235    fn test_org_role_str_roundtrip() {
236        for role in [OrgRole::Member, OrgRole::Admin, OrgRole::Owner] {
237            let s = role.as_str();
238            let parsed: OrgRole = s.parse().unwrap();
239            assert_eq!(parsed, role);
240        }
241    }
242
243    #[test]
244    fn test_org_role_default() {
245        assert_eq!(OrgRole::default(), OrgRole::Owner);
246    }
247}