Skip to main content

unitycatalog_server/api/
mod.rs

1pub use agent_skills::AgentSkillHandler;
2pub use agents::AgentHandler;
3pub use catalogs::CatalogHandler;
4pub use credentials::CredentialHandler;
5pub use entity_tag_assignments::EntityTagAssignmentHandler;
6pub use external_locations::ExternalLocationHandler;
7pub use functions::FunctionHandler;
8pub use model_versions::ModelVersionHandler;
9pub use policies::PolicyHandler;
10pub use registered_models::RegisteredModelHandler;
11pub use schemas::SchemaHandler;
12pub use staging_tables::StagingTableHandler;
13pub use tables::TableHandler;
14pub use tag_policies::TagPolicyHandler;
15pub use temporary_credentials::TemporaryCredentialHandler;
16pub use volumes::VolumeHandler;
17
18use crate::policy::{Permission, Principal};
19use unitycatalog_common::models::ResourceIdent;
20
21// TODO: implement once AssociationLabel::CreatedBy and AssociationLabel::UpdatedBy variants are
22// added to unitycatalog_common (they are currently absent from the AssociationLabel enum in
23// crates/common/src/models/mod.rs). Once those variants exist, this function should be called
24// from create_* and update_* handlers to record who created or last updated a resource.
25//
26// Proposed signature:
27// pub async fn record_audit<S: ResourceStore>(
28//     store: &S,
29//     resource: &ResourceIdent,
30//     principal: &Principal,
31//     action: &str, // "created" | "updated"
32// ) -> Result<()>
33
34pub mod agent_skills;
35pub mod agents;
36pub mod catalogs;
37pub mod credentials;
38pub mod entity_tag_assignments;
39pub mod external_locations;
40pub mod functions;
41pub mod model_versions;
42pub mod policies;
43pub mod registered_models;
44pub mod schemas;
45pub mod staging_tables;
46pub mod tables;
47pub mod tag_policies;
48pub mod temporary_credentials;
49pub mod volumes;
50
51/// Translate a list request's `max_results` into a store page size.
52///
53/// Unset and `0` both mean "use the server default", which the store reads as `None`.
54/// Negative values are rejected.
55pub(crate) fn page_size(max_results: Option<i32>) -> crate::Result<Option<usize>> {
56    match max_results {
57        Some(v) if v < 0 => Err(crate::Error::invalid_argument(
58            "max_results must not be negative",
59        )),
60        Some(v) if v > 0 => Ok(Some(v as usize)),
61        _ => Ok(None),
62    }
63}
64
65/// The fields an update request's `update_mask` selects.
66///
67/// An empty mask or `*` selects every field set in the request ([`UpdateMask::All`]);
68/// otherwise only the listed fields are selected, and a listed field that is unset in the
69/// request is cleared. Names outside `updatable` are rejected.
70pub(crate) enum UpdateMask<'a> {
71    All,
72    Fields(Vec<&'a str>),
73}
74
75impl<'a> UpdateMask<'a> {
76    pub(crate) fn parse(mask: Option<&'a str>, updatable: &[&str]) -> crate::Result<Self> {
77        let fields: Vec<&str> = mask
78            .unwrap_or_default()
79            .split(',')
80            .map(str::trim)
81            .filter(|f| !f.is_empty())
82            .collect();
83        if fields.is_empty() || fields == ["*"] {
84            return Ok(Self::All);
85        }
86        if let Some(unknown) = fields.iter().find(|f| !updatable.contains(f)) {
87            return Err(crate::Error::invalid_argument(format!(
88                "update_mask names a field that cannot be updated: '{unknown}'"
89            )));
90        }
91        Ok(Self::Fields(fields))
92    }
93
94    /// Whether `field` is explicitly listed in the mask.
95    pub(crate) fn names(&self, field: &str) -> bool {
96        matches!(self, Self::Fields(fields) if fields.contains(&field))
97    }
98}
99
100#[derive(Debug, Clone)]
101pub struct RequestContext {
102    pub recipient: Principal,
103}
104
105impl RequestContext {
106    pub fn recipient(&self) -> &Principal {
107        &self.recipient
108    }
109}
110
111impl AsRef<Principal> for RequestContext {
112    fn as_ref(&self) -> &Principal {
113        &self.recipient
114    }
115}
116
117// The storage-proxy client arm requires its backend context to expose a
118// forwarded identity. Gated on `bin`, the only feature that pulls in the
119// `unitycatalog-storage-proxy` dependency (and mounts the client arm in `run`).
120#[cfg(feature = "bin")]
121impl unitycatalog_storage_proxy::ForwardedUser for RequestContext {
122    fn forwarded_user(&self) -> Option<&str> {
123        match &self.recipient {
124            Principal::User(name) => Some(name),
125            Principal::Anonymous => None,
126        }
127    }
128}
129
130pub trait SecuredAction: Send + Sync {
131    /// The resource that the action is performed on.
132    fn resource(&self) -> ResourceIdent;
133
134    /// The permission required to perform the action.
135    fn permission(&self) -> &'static Permission;
136}
137
138#[cfg(test)]
139mod tests {
140    use super::page_size;
141
142    #[test]
143    fn test_page_size() {
144        assert_eq!(page_size(None).unwrap(), None);
145        assert_eq!(page_size(Some(0)).unwrap(), None);
146        assert_eq!(page_size(Some(25)).unwrap(), Some(25));
147        assert!(page_size(Some(-1)).is_err());
148    }
149}