riffdb_client_rust/
capability.rs1use std::error::Error;
4use std::fmt;
5
6use riffdb_proto::v1;
7use riffdb_proto::validate_create_capability_template;
8use riffdb_types::RequestId;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum CapabilityCreateTemplateError {
13 NonemptyRequestId,
15 WrongMode,
17 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
33pub struct NormalCapabilityCreateTemplate {
35 request: v1::CreateCapabilityRequest,
36}
37
38impl NormalCapabilityCreateTemplate {
39 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
58pub struct BootstrapCapabilityCreateTemplate {
60 request: v1::CreateCapabilityRequest,
61}
62
63impl BootstrapCapabilityCreateTemplate {
64 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}