Skip to main content

unitycatalog_server/api/
recipients.rs

1use itertools::Itertools;
2
3use unitycatalog_common::models::ObjectLabel;
4use unitycatalog_common::models::recipients::v1::*;
5use unitycatalog_common::models::{ResourceIdent, ResourceName, ResourceRef};
6
7use super::{RequestContext, SecuredAction};
8use crate::Result;
9pub use crate::codegen::recipients::RecipientHandler;
10use crate::policy::{Permission, Policy, process_resources};
11use crate::store::ResourceStore;
12
13#[async_trait::async_trait]
14impl<T: ResourceStore + Policy<RequestContext>> RecipientHandler<RequestContext> for T {
15    #[tracing::instrument(skip(self, context), fields(resource_name))]
16    async fn create_recipient(
17        &self,
18        request: CreateRecipientRequest,
19        context: RequestContext,
20    ) -> Result<Recipient> {
21        tracing::Span::current().record("resource_name", &request.name);
22        self.check_required(&request, &context).await?;
23        let resource = Recipient {
24            name: request.name,
25            authentication_type: request.authentication_type,
26            comment: request.comment,
27            properties: request.properties,
28            ..Default::default()
29        };
30
31        // When authentication_type is TOKEN, an initial RecipientToken should be created
32        // here and stored via the SecretManager.  The token's activation_url would be
33        // returned to the caller so they can share it with the recipient.  This requires
34        // a SecretManager to be accessible from the handler — once that is available,
35        // generate a token, store it with the given expiration_time, and embed it in the
36        // returned Recipient's `tokens` field.  Until then, TOKEN-type recipients are
37        // created without a pre-generated token; they can be activated out-of-band.
38
39        let info = self.create(resource.into()).await?.0.try_into()?;
40        Ok(info)
41    }
42
43    #[tracing::instrument(skip(self, context), fields(resource_name))]
44    async fn delete_recipient(
45        &self,
46        request: DeleteRecipientRequest,
47        context: RequestContext,
48    ) -> Result<()> {
49        tracing::Span::current().record("resource_name", &request.name);
50        self.check_required(&request, &context).await?;
51        Ok(self.delete(&request.resource()).await?)
52    }
53
54    #[tracing::instrument(skip(self, context), fields(resource_name))]
55    async fn get_recipient(
56        &self,
57        request: GetRecipientRequest,
58        context: RequestContext,
59    ) -> Result<Recipient> {
60        tracing::Span::current().record("resource_name", &request.name);
61        self.check_required(&request, &context).await?;
62        Ok(self.get(&request.resource()).await?.0.try_into()?)
63    }
64
65    #[tracing::instrument(skip(self, context))]
66    async fn list_recipients(
67        &self,
68        request: ListRecipientsRequest,
69        context: RequestContext,
70    ) -> Result<ListRecipientsResponse> {
71        self.check_required(&request, &context).await?;
72        let (mut resources, next_page_token) = self
73            .list(
74                &ObjectLabel::Recipient,
75                None,
76                request.max_results.map(|v| v as usize),
77                request.page_token,
78            )
79            .await?;
80        process_resources(self, &context, &Permission::Read, &mut resources).await?;
81        Ok(ListRecipientsResponse {
82            recipients: resources.into_iter().map(|r| r.try_into()).try_collect()?,
83            next_page_token,
84            ..Default::default()
85        })
86    }
87
88    #[tracing::instrument(skip(self, context), fields(resource_name))]
89    async fn update_recipient(
90        &self,
91        request: UpdateRecipientRequest,
92        context: RequestContext,
93    ) -> Result<Recipient> {
94        tracing::Span::current().record("resource_name", &request.name);
95        self.check_required(&request, &context).await?;
96        let ident = request.resource();
97        let current: Recipient = self.get(&ident).await?.0.try_into()?;
98
99        // Apply the mutable fields from the request onto the existing recipient.
100        // Token expiration updates (request.expiration_time) are acknowledged here but
101        // cannot yet be persisted: the `Recipient` protobuf stores tokens as
102        // `Vec<RecipientToken>` and token lifecycle management (create / rotate / expire)
103        // requires a SecretManager, which is not available through the ResourceStore +
104        // Policy bound used by RecipientHandler.  Token handling should be added once
105        // a dedicated token-management service is wired into the handler context.
106        let updated = Recipient {
107            name: request.new_name.unwrap_or(request.name),
108            owner: request.owner.or(current.owner),
109            comment: request.comment.or(current.comment),
110            properties: if request.properties.is_empty() {
111                current.properties
112            } else {
113                request.properties
114            },
115            // Preserve all fields managed by the store.
116            ..current
117        };
118
119        Ok(self.update(&ident, updated.into()).await?.0.try_into()?)
120    }
121}
122
123impl SecuredAction for CreateRecipientRequest {
124    fn resource(&self) -> ResourceIdent {
125        ResourceIdent::recipient(ResourceName::new([self.name.as_str()]))
126    }
127
128    fn permission(&self) -> &'static Permission {
129        &Permission::Create
130    }
131}
132
133impl SecuredAction for ListRecipientsRequest {
134    fn resource(&self) -> ResourceIdent {
135        ResourceIdent::recipient(ResourceRef::Undefined)
136    }
137
138    fn permission(&self) -> &'static Permission {
139        &Permission::Read
140    }
141}
142
143impl SecuredAction for GetRecipientRequest {
144    fn resource(&self) -> ResourceIdent {
145        ResourceIdent::recipient(ResourceName::new([self.name.as_str()]))
146    }
147
148    fn permission(&self) -> &'static Permission {
149        &Permission::Read
150    }
151}
152
153impl SecuredAction for UpdateRecipientRequest {
154    fn resource(&self) -> ResourceIdent {
155        ResourceIdent::recipient(ResourceName::new([self.name.as_str()]))
156    }
157
158    fn permission(&self) -> &'static Permission {
159        &Permission::Manage
160    }
161}
162
163impl SecuredAction for DeleteRecipientRequest {
164    fn resource(&self) -> ResourceIdent {
165        ResourceIdent::recipient(ResourceName::new([self.name.as_str()]))
166    }
167
168    fn permission(&self) -> &'static Permission {
169        &Permission::Manage
170    }
171}