Skip to main content

riffdb_client_rust/
capability.rs

1//! Immutable capability-create templates for bounded transport retry.
2
3use std::error::Error;
4use std::fmt;
5
6use riffdb_proto::v1;
7use riffdb_proto::validate_create_capability_template;
8use riffdb_types::RequestId;
9
10/// A closed local failure while constructing a capability-create template.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum CapabilityCreateTemplateError {
13    /// The caller supplied an outer request ID instead of the empty sentinel.
14    NonemptyRequestId,
15    /// The request mode does not match the selected template type.
16    WrongMode,
17    /// The retained semantic body violates the public protocol.
18    InvalidBody,
19}
20
21impl fmt::Display for CapabilityCreateTemplateError {
22    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23        formatter.write_str(match self {
24            Self::NonemptyRequestId => "capability-create template request ID is not empty",
25            Self::WrongMode => "capability-create template mode is invalid",
26            Self::InvalidBody => "capability-create template body is invalid",
27        })
28    }
29}
30
31impl Error for CapabilityCreateTemplateError {}
32
33/// A checked immutable normal capability-create operation.
34pub struct NormalCapabilityCreateTemplate {
35    request: v1::CreateCapabilityRequest,
36}
37
38impl NormalCapabilityCreateTemplate {
39    /// Validates and retains one exact normal capability-create body.
40    pub fn new(
41        request: v1::CreateCapabilityRequest,
42    ) -> Result<Self, CapabilityCreateTemplateError> {
43        validate_template_mode(&request, v1::CapabilityCreateMode::Normal)?;
44        Ok(Self { request })
45    }
46
47    pub(crate) fn request(&self, request_id: RequestId) -> v1::CreateCapabilityRequest {
48        request_with_id(&self.request, request_id)
49    }
50}
51
52impl fmt::Debug for NormalCapabilityCreateTemplate {
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        formatter.write_str("NormalCapabilityCreateTemplate([REDACTED])")
55    }
56}
57
58/// A checked immutable bootstrap capability-create operation.
59pub struct BootstrapCapabilityCreateTemplate {
60    request: v1::CreateCapabilityRequest,
61}
62
63impl BootstrapCapabilityCreateTemplate {
64    /// Validates and retains one exact bootstrap capability-create body.
65    pub fn new(
66        request: v1::CreateCapabilityRequest,
67    ) -> Result<Self, CapabilityCreateTemplateError> {
68        validate_template_mode(&request, v1::CapabilityCreateMode::Bootstrap)?;
69        Ok(Self { request })
70    }
71
72    pub(crate) fn request(&self, request_id: RequestId) -> v1::CreateCapabilityRequest {
73        request_with_id(&self.request, request_id)
74    }
75}
76
77impl fmt::Debug for BootstrapCapabilityCreateTemplate {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        formatter.write_str("BootstrapCapabilityCreateTemplate([REDACTED])")
80    }
81}
82
83fn validate_template_mode(
84    request: &v1::CreateCapabilityRequest,
85    expected_mode: v1::CapabilityCreateMode,
86) -> Result<(), CapabilityCreateTemplateError> {
87    if !request.request_id.is_empty() {
88        return Err(CapabilityCreateTemplateError::NonemptyRequestId);
89    }
90    if request.mode != expected_mode as i32 {
91        return Err(CapabilityCreateTemplateError::WrongMode);
92    }
93    validate_create_capability_template(request)
94        .map_err(|_| CapabilityCreateTemplateError::InvalidBody)
95}
96
97fn request_with_id(
98    template: &v1::CreateCapabilityRequest,
99    request_id: RequestId,
100) -> v1::CreateCapabilityRequest {
101    let mut request = template.clone();
102    request.request_id = request_id.into_bytes().to_vec();
103    request
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    fn request(mode: v1::CapabilityCreateMode) -> v1::CreateCapabilityRequest {
111        let permissions = if mode == v1::CapabilityCreateMode::Bootstrap {
112            vec![v1::CapabilityPermission {
113                permission: Some(
114                    v1::capability_permission::Permission::AdministerCapabilities(v1::Unit {}),
115                ),
116            }]
117        } else {
118            Vec::new()
119        };
120        v1::CreateCapabilityRequest {
121            request_id: Vec::new(),
122            mode: mode as i32,
123            capability_id: RequestId::from_unix_milliseconds_and_random(1, [1; 10])
124                .expect("UUIDv7")
125                .into_bytes()
126                .to_vec(),
127            principal_id: "operator".to_owned(),
128            actor_kind: v1::ActorKind::Human as i32,
129            requested_lifetime_seconds: 60,
130            audiences: vec!["riffdb-cli".to_owned()],
131            grant: Some(v1::CapabilityGrant {
132                tenant_scope: Some(v1::TenantScope {
133                    scope: Some(v1::tenant_scope::Scope::Global(v1::Unit {})),
134                }),
135                partition_scope: Some(v1::PartitionScope {
136                    scope: Some(v1::partition_scope::Scope::All(v1::Unit {})),
137                }),
138                permissions,
139                field_visibility: Vec::new(),
140                max_scan_rows: 1,
141                approval_required: Vec::new(),
142                row_policy: None,
143                export: None,
144                reimport: None,
145                vector_inspection: None,
146            }),
147        }
148    }
149
150    #[test]
151    fn constructor_enforces_empty_id_exact_mode_and_complete_body() {
152        assert!(
153            NormalCapabilityCreateTemplate::new(request(v1::CapabilityCreateMode::Normal)).is_ok()
154        );
155        assert!(
156            BootstrapCapabilityCreateTemplate::new(request(v1::CapabilityCreateMode::Bootstrap))
157                .is_ok()
158        );
159
160        let mut nonempty = request(v1::CapabilityCreateMode::Normal);
161        nonempty.request_id = vec![1; 16];
162        assert!(matches!(
163            NormalCapabilityCreateTemplate::new(nonempty),
164            Err(CapabilityCreateTemplateError::NonemptyRequestId)
165        ));
166        assert!(matches!(
167            NormalCapabilityCreateTemplate::new(request(v1::CapabilityCreateMode::Bootstrap)),
168            Err(CapabilityCreateTemplateError::WrongMode)
169        ));
170        let mut invalid = request(v1::CapabilityCreateMode::Normal);
171        invalid.principal_id.clear();
172        assert!(matches!(
173            NormalCapabilityCreateTemplate::new(invalid),
174            Err(CapabilityCreateTemplateError::InvalidBody)
175        ));
176    }
177
178    #[test]
179    fn every_submission_changes_only_the_outer_request_id() {
180        let template =
181            NormalCapabilityCreateTemplate::new(request(v1::CapabilityCreateMode::Normal))
182                .expect("template");
183        let first_id = RequestId::from_unix_milliseconds_and_random(2, [2; 10]).expect("ID");
184        let second_id = RequestId::from_unix_milliseconds_and_random(3, [3; 10]).expect("ID");
185        let first = template.request(first_id);
186        let second = template.request(second_id);
187
188        assert_ne!(first.request_id, second.request_id);
189        let mut first_without_id = first;
190        let mut second_without_id = second;
191        first_without_id.request_id.clear();
192        second_without_id.request_id.clear();
193        assert_eq!(first_without_id, second_without_id);
194        assert_eq!(
195            format!("{template:?}"),
196            "NormalCapabilityCreateTemplate([REDACTED])"
197        );
198    }
199}