1pub mod v1 {
4 tonic::include_proto!("anvil.v1");
5}
6
7#[cfg(test)]
8mod tests {
9 use super::v1::{DeletedObject, NeverExisted, ObjectHead, PresentObject, object_head};
10
11 #[test]
12 fn exact_path_states_are_distinct() {
13 let states = [
14 ObjectHead {
15 state: Some(object_head::State::Present(PresentObject {
16 version: 7,
17 content_hash: vec![7; 32],
18 content_length: 99,
19 content_type: String::new(),
20 })),
21 },
22 ObjectHead {
23 state: Some(object_head::State::Deleted(DeletedObject { version: 8 })),
24 },
25 ObjectHead {
26 state: Some(object_head::State::NeverExisted(NeverExisted {})),
27 },
28 ];
29
30 assert!(matches!(
31 &states[0].state,
32 Some(object_head::State::Present(_))
33 ));
34 assert!(matches!(
35 &states[1].state,
36 Some(object_head::State::Deleted(_))
37 ));
38 assert!(matches!(
39 &states[2].state,
40 Some(object_head::State::NeverExisted(_))
41 ));
42 }
43
44 #[test]
45 fn schema_keeps_removed_capabilities_out() {
46 let schema = include_str!("../proto/anvil.proto").to_ascii_lowercase();
47 for forbidden in [
48 "rpc uploadblob",
49 "rpc publishobject",
50 "rpc putobject",
51 "message blobref",
52 "rpc listprefix",
53 "rpc begintransaction",
54 "rpc committransaction",
55 ] {
56 assert!(!schema.contains(forbidden), "schema contains `{forbidden}`");
57 }
58
59 assert!(schema.contains("executor_nomination_log_index"));
60 assert!(schema.contains("commit_log_index"));
61 assert!(schema.contains("immutable_path_prefixes"));
62 assert!(schema.contains("program_only_path_prefixes"));
63 assert!(!schema.contains("rpc registerprogram"));
64 assert!(!schema.contains("message registerprogram"));
65 assert!(schema.contains("objectaddress program"));
66 assert!(schema.contains("_anvil/programs/{name}@{version}"));
67 assert!(schema.contains("rpc startput(putheader) returns (puttoken)"));
68 assert!(schema.contains("rpc put(stream putrequest) returns (puttoken)"));
69 assert!(schema.contains("rpc putend(puttoken) returns (mutationreceipt)"));
70
71 for rpc in [
72 "rpc exchangeclientcredentials",
73 "rpc provisiontenant",
74 "rpc createapplication",
75 "rpc rotateapplicationcredential",
76 "rpc disableapplicationcredential",
77 "rpc createbucket",
78 "rpc grantapplicationrole",
79 "rpc revokeapplicationrole",
80 "rpc putschema",
81 "rpc bindschema",
82 "rpc getbinding",
83 "rpc getschema",
84 "rpc mutatetuples",
85 "rpc readtuples",
86 "rpc checkpermission",
87 "rpc checkpermissions",
88 "rpc watchprefix",
89 "rpc listobjects",
90 "rpc deleteversion",
91 "rpc listobjectversions",
92 "rpc setbucketversioning",
93 ] {
94 assert!(schema.contains(rpc), "schema is missing `{rpc}`");
95 }
96 for forbidden in [
97 "rpc createrealm",
98 "rpc deleterealm",
99 "rpc applyschema",
100 "zookie",
101 "caveat",
102 "publication_metadata",
103 "insecure_no_auth",
104 "api_token",
105 ] {
106 assert!(!schema.contains(forbidden), "schema contains `{forbidden}`");
107 }
108 }
109
110 #[test]
111 fn object_surface_has_only_explicit_typed_mutations() {
112 use super::v1::{
113 BulkOperation, BulkPutIfVersionRequest, CreateBucketRequest, DeleteIfVersionRequest,
114 DeleteRequest, DeleteVersionRequest, DeleteVersionResponse, Durability,
115 ListObjectsRequest, ListObjectsResponse, ObjectAddress, ObjectVersioning, PutHeader,
116 PutIfVersionOperation, PutRequest, PutToken, bulk_operation, put_header,
117 };
118
119 let address = Some(ObjectAddress {
120 tenant: "acme".into(),
121 bucket: "objects".into(),
122 path: "one".into(),
123 });
124 let header = PutHeader {
125 address: address.clone(),
126 content_type: "application/json".into(),
127 command_id: "command-1".into(),
128 durability: Durability::Local as i32,
129 operation: Some(put_header::Operation::PutIfVersion(PutIfVersionOperation {
130 expected_version: 8,
131 })),
132 };
133 let frame = PutRequest {
134 token: Some(PutToken {
135 value: b"opaque".to_vec(),
136 expires_at: None,
137 }),
138 chunk: Vec::new(),
139 };
140 assert!(matches!(
141 header.operation,
142 Some(put_header::Operation::PutIfVersion(_))
143 ));
144 assert!(frame.chunk.is_empty());
145
146 let operations = [
147 bulk_operation::Operation::Put(Default::default()),
148 bulk_operation::Operation::PutIfAbsent(Default::default()),
149 bulk_operation::Operation::PutIfVersion(BulkPutIfVersionRequest::default()),
150 bulk_operation::Operation::PutImmutable(Default::default()),
151 bulk_operation::Operation::Delete(DeleteRequest {
152 address: address.clone(),
153 ..Default::default()
154 }),
155 bulk_operation::Operation::DeleteIfVersion(DeleteIfVersionRequest {
156 address: address.clone(),
157 expected_version: 8,
158 ..Default::default()
159 }),
160 ];
161 assert_eq!(
162 operations
163 .into_iter()
164 .map(|operation| BulkOperation {
165 operation: Some(operation),
166 })
167 .count(),
168 6
169 );
170
171 let current_head_delete = DeleteIfVersionRequest {
172 address: address.clone(),
173 expected_version: 8,
174 ..Default::default()
175 };
176 let retained_version_delete = DeleteVersionRequest {
177 address,
178 version: 7,
179 ..Default::default()
180 };
181 assert_eq!(current_head_delete.expected_version, 8);
182 assert_eq!(retained_version_delete.version, 7);
183 let replaced_current = DeleteVersionResponse {
184 deleted: true,
185 replacement_tombstone_version: Some(9),
186 };
187 assert_eq!(replaced_current.replacement_tombstone_version, Some(9));
188 assert_eq!(
189 CreateBucketRequest::default().versioning,
190 ObjectVersioning::Unversioned as i32
191 );
192
193 let list_request = ListObjectsRequest {
194 tenant: "acme".into(),
195 bucket: "objects".into(),
196 prefix: "reports/".into(),
197 start_after: Some("reports/2025.json".into()),
198 limit: 100,
199 };
200 let list_response = ListObjectsResponse {
201 paths: vec!["reports/2026.json".into()],
202 has_more: false,
203 };
204 assert_eq!(
205 list_request.start_after.as_deref(),
206 Some("reports/2025.json")
207 );
208 assert_eq!(list_response.paths, vec!["reports/2026.json".to_owned()]);
209 }
210
211 #[test]
212 fn authorization_wire_types_preserve_scope_and_typed_unions() {
213 use super::v1::{
214 AnyUsersetSelector, AtLeastRevision, AuthzConsistency, AuthzScope, DirectRelation,
215 InheritRule, NamespaceDefinition, ObjectRef, Permission, PermissionRule,
216 PutSchemaRequest, RelationDefinition, SubjectSelector, Userset, authz_consistency,
217 object_ref, permission_rule, relation_definition, subject, subject_selector,
218 };
219
220 let account = ObjectRef {
221 namespace: "account".into(),
222 id: Some(object_ref::Id::OpaqueId("acme".into())),
223 };
224 let members = Userset {
225 object: Some(account),
226 relation: "member".into(),
227 };
228 let schema = NamespaceDefinition {
229 name: "ledger".into(),
230 relations: vec![
231 RelationDefinition {
232 name: "reader".into(),
233 kind: Some(relation_definition::Kind::Direct(DirectRelation {
234 allowed_subjects: vec![SubjectSelector {
235 selector: Some(subject_selector::Selector::AnyUserset(
236 AnyUsersetSelector {
237 namespace: "account".into(),
238 relation: "member".into(),
239 },
240 )),
241 }],
242 })),
243 },
244 RelationDefinition {
245 name: "read".into(),
246 kind: Some(relation_definition::Kind::Permission(Permission {
247 rules: vec![PermissionRule {
248 rule: Some(permission_rule::Rule::Inherit(InheritRule {
249 relation: "reader".into(),
250 })),
251 }],
252 })),
253 },
254 ],
255 };
256 let subject = super::v1::Subject {
257 kind: Some(subject::Kind::Userset(members)),
258 };
259 let publication = PutSchemaRequest {
260 schema_id: "acme".into(),
261 namespaces: vec![schema],
262 };
263 let consistency = AuthzConsistency {
264 requirement: Some(authz_consistency::Requirement::AtLeast(AtLeastRevision {
265 revision: 42,
266 })),
267 };
268 let system_scope = AuthzScope {
269 storage_tenant: "anvil-internal".into(),
270 realm: "_anvil/system".into(),
271 };
272
273 assert_eq!(publication.namespaces[0].relations.len(), 2);
274 assert!(matches!(subject.kind, Some(subject::Kind::Userset(_))));
275 assert!(matches!(
276 consistency.requirement,
277 Some(authz_consistency::Requirement::AtLeast(AtLeastRevision {
278 revision: 42
279 }))
280 ));
281 assert_eq!(system_scope.realm, "_anvil/system");
282 }
283
284 #[test]
285 fn tuple_mutation_and_batch_checks_share_request_scope() {
286 use super::v1::{
287 AuthzScope, CheckPermissionsRequest, MutateTuplesRequest, ObjectRef, PermissionCheck,
288 RelationTuple, Subject, TupleMutation, authz_consistency, object_ref, subject,
289 tuple_mutation,
290 };
291
292 let scope = AuthzScope {
293 storage_tenant: "acme".into(),
294 realm: "default".into(),
295 };
296 let ledger = ObjectRef {
297 namespace: "ledger".into(),
298 id: Some(object_ref::Id::OpaqueId("main".into())),
299 };
300 let alice = Subject {
301 kind: Some(subject::Kind::Object(ObjectRef {
302 namespace: "user".into(),
303 id: Some(object_ref::Id::OpaqueId("alice".into())),
304 })),
305 };
306 let tuple = RelationTuple {
307 object: Some(ledger.clone()),
308 relation: "reader".into(),
309 subject: Some(alice.clone()),
310 };
311 let mutation = MutateTuplesRequest {
312 scope: Some(scope.clone()),
313 operation_id: "grant-alice".into(),
314 expected_revision: Some(41),
315 mutations: vec![TupleMutation {
316 operation: Some(tuple_mutation::Operation::Add(tuple)),
317 }],
318 };
319 let checks = CheckPermissionsRequest {
320 scope: Some(scope),
321 checks: vec![PermissionCheck {
322 subject: Some(alice),
323 object: Some(ledger),
324 relation: "read".into(),
325 }],
326 consistency: Some(super::v1::AuthzConsistency {
327 requirement: Some(authz_consistency::Requirement::Exact(
328 super::v1::ExactRevision { revision: 42 },
329 )),
330 }),
331 };
332
333 assert_eq!(mutation.expected_revision, Some(41));
334 assert_eq!(mutation.mutations.len(), 1);
335 assert_eq!(checks.checks.len(), 1);
336 assert!(matches!(
337 checks
338 .consistency
339 .and_then(|consistency| consistency.requirement),
340 Some(authz_consistency::Requirement::Exact(_))
341 ));
342 }
343}