Skip to main content

modkit_db/secure/
entity_traits.rs

1use sea_orm::EntityTrait;
2
3/// Defines the contract for entities that can be scoped by tenant, resource, owner, and type.
4///
5/// Each entity implementing this trait must explicitly declare all four scope dimensions:
6/// - `tenant_col()`: Column for tenant-based isolation (multi-tenancy)
7/// - `resource_col()`: Column for resource-level access (typically the primary key)
8/// - `owner_col()`: Column for owner-based filtering
9/// - `type_col()`: Column for type-based filtering
10///
11/// **Important**: No implicit defaults are allowed. Every scope dimension must be explicitly
12/// specified as `Some(Column::...)` or `None` to enforce compile-time safety in secure systems.
13///
14/// # Example (Manual Implementation)
15/// ```rust,ignore
16/// impl ScopableEntity for user::Entity {
17///     fn tenant_col() -> Option<Self::Column> {
18///         Some(user::Column::TenantId)
19///     }
20///     fn resource_col() -> Option<Self::Column> {
21///         Some(user::Column::Id)
22///     }
23///     fn owner_col() -> Option<Self::Column> {
24///         None
25///     }
26///     fn type_col() -> Option<Self::Column> {
27///         None
28///     }
29///     fn resolve_property(property: &str) -> Option<Self::Column> {
30///         match property {
31///             "owner_tenant_id" => Self::tenant_col(),
32///             "id" => Self::resource_col(),
33///             "owner_id" => Self::owner_col(),
34///             _ => None,
35///         }
36///     }
37/// }
38/// ```
39///
40/// # Example (Using Derive Macro)
41/// ```rust,ignore
42/// use modkit_db::secure::Scopable;
43///
44/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Scopable)]
45/// #[sea_orm(table_name = "users")]
46/// #[secure(
47///     tenant_col = "tenant_id",
48///     resource_col = "id",
49///     no_owner,
50///     no_type
51/// )]
52/// pub struct Model {
53///     #[sea_orm(primary_key)]
54///     pub id: Uuid,
55///     pub tenant_id: Uuid,
56///     pub email: String,
57/// }
58/// // Macro auto-generates resolve_property:
59/// //   "owner_tenant_id" => Some(Column::TenantId)  (from tenant_col)
60/// //   "id"              => Some(Column::Id)         (from resource_col)
61/// //   _                 => None
62/// ```
63///
64/// # Custom PEP Properties
65/// ```rust,ignore
66/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Scopable)]
67/// #[sea_orm(table_name = "resources")]
68/// #[secure(
69///     tenant_col = "tenant_id",
70///     resource_col = "id",
71///     no_owner,
72///     no_type,
73///     pep_prop(department_id = "department_id"),
74/// )]
75/// pub struct Model {
76///     #[sea_orm(primary_key)]
77///     pub id: Uuid,
78///     pub tenant_id: Uuid,
79///     pub department_id: Uuid,
80/// }
81/// // Macro auto-generates resolve_property:
82/// //   "owner_tenant_id" => Some(Column::TenantId)      (from tenant_col)
83/// //   "id"              => Some(Column::Id)             (from resource_col)
84/// //   "department_id"   => Some(Column::DepartmentId)   (from pep_prop)
85/// //   _                 => None
86/// ```
87///
88/// # Unrestricted Entities
89/// ```rust,ignore
90/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Scopable)]
91/// #[sea_orm(table_name = "system_config")]
92/// #[secure(unrestricted)]
93/// pub struct Model {
94///     #[sea_orm(primary_key)]
95///     pub id: Uuid,
96///     pub config_key: String,
97/// }
98/// ```
99pub trait ScopableEntity: EntityTrait {
100    /// Indicates whether this entity is explicitly marked as unrestricted.
101    ///
102    /// This is a compile-time flag set via `#[secure(unrestricted)]` that documents
103    /// the entity's global nature (e.g., system configuration, lookup tables).
104    ///
105    /// When `IS_UNRESTRICTED` is true, all column methods return `None`.
106    ///
107    /// Default: `false` (entity participates in scoping logic)
108    const IS_UNRESTRICTED: bool = false;
109
110    /// Returns the column that stores the tenant identifier.
111    ///
112    /// - Multi-tenant entities: `Some(Column::TenantId)`
113    /// - Global/system entities: `None`
114    ///
115    /// Must be explicitly specified via `tenant_col = "..."` or `no_tenant`.
116    fn tenant_col() -> Option<Self::Column>;
117
118    /// Returns the column that stores the primary resource identifier.
119    ///
120    /// Typically the primary key column (e.g., `Column::Id`).
121    ///
122    /// Must be explicitly specified via `resource_col = "..."` or `no_resource`.
123    fn resource_col() -> Option<Self::Column>;
124
125    /// Returns the column that stores the resource owner identifier.
126    ///
127    /// Used for owner-based access control policies.
128    ///
129    /// Must be explicitly specified via `owner_col = "..."` or `no_owner`.
130    fn owner_col() -> Option<Self::Column>;
131
132    /// Returns the column that stores the resource type identifier.
133    ///
134    /// Used for type-based filtering in polymorphic scenarios.
135    ///
136    /// Must be explicitly specified via `type_col = "..."` or `no_type`.
137    fn type_col() -> Option<Self::Column>;
138
139    /// Resolve an authorization property name to a database column.
140    ///
141    /// Maps PEP property names (e.g. `"owner_tenant_id"`) to `SeaORM` columns
142    /// so the scope condition builder can translate `AccessScope` constraints
143    /// into SQL `WHERE` clauses.
144    ///
145    /// When using `#[derive(Scopable)]`, this method is auto-generated from
146    /// dimension columns and `pep_prop(...)` entries:
147    /// - `tenant_col` → `"owner_tenant_id"`
148    /// - `resource_col` → `"id"`
149    /// - `owner_col` → `"owner_id"`
150    /// - `pep_prop(custom = "column")` → `"custom"`
151    ///
152    /// Manual implementors must provide all property arms explicitly.
153    #[must_use]
154    fn resolve_property(property: &str) -> Option<Self::Column>;
155}