olai-uc-server 0.0.9

Unity Catalog REST server with pluggable storage backends.
Documentation
pub use agent_skills::AgentSkillHandler;
pub use agents::AgentHandler;
pub use catalogs::CatalogHandler;
pub use credentials::CredentialHandler;
pub use entity_tag_assignments::EntityTagAssignmentHandler;
pub use external_locations::ExternalLocationHandler;
pub use functions::FunctionHandler;
pub use model_versions::ModelVersionHandler;
pub use policies::PolicyHandler;
pub use registered_models::RegisteredModelHandler;
pub use schemas::SchemaHandler;
pub use staging_tables::StagingTableHandler;
pub use tables::TableHandler;
pub use tag_policies::TagPolicyHandler;
pub use temporary_credentials::TemporaryCredentialHandler;
pub use volumes::VolumeHandler;

use crate::policy::{Permission, Principal};
use unitycatalog_common::models::ResourceIdent;

// TODO: implement once AssociationLabel::CreatedBy and AssociationLabel::UpdatedBy variants are
// added to unitycatalog_common (they are currently absent from the AssociationLabel enum in
// crates/common/src/models/mod.rs). Once those variants exist, this function should be called
// from create_* and update_* handlers to record who created or last updated a resource.
//
// Proposed signature:
// pub async fn record_audit<S: ResourceStore>(
//     store: &S,
//     resource: &ResourceIdent,
//     principal: &Principal,
//     action: &str, // "created" | "updated"
// ) -> Result<()>

pub mod agent_skills;
pub mod agents;
pub mod catalogs;
pub mod credentials;
pub mod entity_tag_assignments;
pub mod external_locations;
pub mod functions;
pub mod model_versions;
pub mod policies;
pub mod registered_models;
pub mod schemas;
pub mod staging_tables;
pub mod tables;
pub mod tag_policies;
pub mod temporary_credentials;
pub mod volumes;

/// Translate a list request's `max_results` into a store page size.
///
/// Unset and `0` both mean "use the server default", which the store reads as `None`.
/// Negative values are rejected.
pub(crate) fn page_size(max_results: Option<i32>) -> crate::Result<Option<usize>> {
    match max_results {
        Some(v) if v < 0 => Err(crate::Error::invalid_argument(
            "max_results must not be negative",
        )),
        Some(v) if v > 0 => Ok(Some(v as usize)),
        _ => Ok(None),
    }
}

/// The fields an update request's `update_mask` selects.
///
/// An empty mask or `*` selects every field set in the request ([`UpdateMask::All`]);
/// otherwise only the listed fields are selected, and a listed field that is unset in the
/// request is cleared. Names outside `updatable` are rejected.
pub(crate) enum UpdateMask<'a> {
    All,
    Fields(Vec<&'a str>),
}

impl<'a> UpdateMask<'a> {
    pub(crate) fn parse(mask: Option<&'a str>, updatable: &[&str]) -> crate::Result<Self> {
        let fields: Vec<&str> = mask
            .unwrap_or_default()
            .split(',')
            .map(str::trim)
            .filter(|f| !f.is_empty())
            .collect();
        if fields.is_empty() || fields == ["*"] {
            return Ok(Self::All);
        }
        if let Some(unknown) = fields.iter().find(|f| !updatable.contains(f)) {
            return Err(crate::Error::invalid_argument(format!(
                "update_mask names a field that cannot be updated: '{unknown}'"
            )));
        }
        Ok(Self::Fields(fields))
    }

    /// Whether `field` is explicitly listed in the mask.
    pub(crate) fn names(&self, field: &str) -> bool {
        matches!(self, Self::Fields(fields) if fields.contains(&field))
    }
}

#[derive(Debug, Clone)]
pub struct RequestContext {
    pub recipient: Principal,
}

impl RequestContext {
    pub fn recipient(&self) -> &Principal {
        &self.recipient
    }
}

impl AsRef<Principal> for RequestContext {
    fn as_ref(&self) -> &Principal {
        &self.recipient
    }
}

// The storage-proxy client arm requires its backend context to expose a
// forwarded identity. Gated on `bin`, the only feature that pulls in the
// `unitycatalog-storage-proxy` dependency (and mounts the client arm in `run`).
#[cfg(feature = "bin")]
impl unitycatalog_storage_proxy::ForwardedUser for RequestContext {
    fn forwarded_user(&self) -> Option<&str> {
        match &self.recipient {
            Principal::User(name) => Some(name),
            Principal::Anonymous => None,
        }
    }
}

pub trait SecuredAction: Send + Sync {
    /// The resource that the action is performed on.
    fn resource(&self) -> ResourceIdent;

    /// The permission required to perform the action.
    fn permission(&self) -> &'static Permission;
}

#[cfg(test)]
mod tests {
    use super::page_size;

    #[test]
    fn test_page_size() {
        assert_eq!(page_size(None).unwrap(), None);
        assert_eq!(page_size(Some(0)).unwrap(), None);
        assert_eq!(page_size(Some(25)).unwrap(), Some(25));
        assert!(page_size(Some(-1)).is_err());
    }
}