1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! ACL methods on [`VtaClient`].
use super::{
AclEntryResponse, AclListResponse, CreateAclRequest, SwapAclRequest, UpdateAclRequest,
VtaClient, encode_path_segment,
};
use crate::acl::ContextDirection;
use crate::error::VtaError;
#[cfg(feature = "client")]
use crate::protocols::acl_management;
#[cfg(feature = "client")]
impl VtaClient {
/// List ACL entries, optionally filtered to the entries that may act **in**
/// `context`.
///
/// For the other direction — what is granted *beneath* a context, which is
/// what a revocation sweep or a delegation audit needs — call
/// [`list_acl_in_direction`](Self::list_acl_in_direction). This method is
/// exactly `ContextDirection::ActingIn` and stays that way.
pub async fn list_acl(&self, context: Option<&str>) -> Result<AclListResponse, VtaError> {
self.list_acl_in_direction(context, ContextDirection::ActingIn)
.await
}
/// List ACL entries, filtering `context` in an explicit
/// [`ContextDirection`].
///
/// A context id names a subtree, so `?context=X` is two questions:
/// [`ActingIn`](ContextDirection::ActingIn) (who holds authority over X)
/// and [`Subtree`](ContextDirection::Subtree) (what is granted inside X).
/// Sweeping a subtree with the first returns precisely the entries the
/// sweep is *not* revoking, so a caller cutting a branch must ask for the
/// second (#822). [`Any`](ContextDirection::Any) is the auditor's union.
///
/// The direction is inert without a context and the VTA refuses that
/// combination rather than silently listing everything.
pub async fn list_acl_in_direction(
&self,
context: Option<&str>,
direction: ContextDirection,
) -> Result<AclListResponse, VtaError> {
// Omitted when it is the default, so an old VTA — which would reject
// the unknown field — keeps serving the calls that mean what it
// already does.
let direction_field = (direction != ContextDirection::default())
.then(|| serde_json::Value::String(direction.to_string()));
self.rpc(
acl_management::LIST_ACL,
serde_json::json!({ "context": context, "direction": direction_field }),
acl_management::LIST_ACL_RESULT,
30,
|c, url| {
// Both parameters are appended independently — a direction
// without a context is a caller error, and the VTA says so;
// dropping it here would make REST answer a question DIDComm
// refuses.
let mut u = format!("{url}/acl");
let mut sep = '?';
if let Some(ctx) = context {
u.push_str(&format!("{sep}context={ctx}"));
sep = '&';
}
if direction != ContextDirection::default() {
u.push_str(&format!("{sep}direction={direction}"));
}
c.get(u)
},
)
.await
}
pub async fn get_acl(&self, did: &str) -> Result<AclEntryResponse, VtaError> {
self.rpc(
acl_management::GET_ACL,
serde_json::json!({ "did": did }),
acl_management::GET_ACL_RESULT,
30,
|c, url| c.get(format!("{url}/acl/{}", encode_path_segment(did))),
)
.await
}
pub async fn create_acl(&self, req: CreateAclRequest) -> Result<AclEntryResponse, VtaError> {
self.rpc(
acl_management::CREATE_ACL,
serde_json::to_value(&req)?,
acl_management::CREATE_ACL_RESULT,
30,
|c, url| c.post(format!("{url}/acl")).json(&req),
)
.await
}
pub async fn update_acl(
&self,
did: &str,
req: UpdateAclRequest,
) -> Result<AclEntryResponse, VtaError> {
self.rpc(
acl_management::UPDATE_ACL,
serde_json::json!({
"did": did,
"role": &req.role,
"label": &req.label,
"allowed_contexts": &req.allowed_contexts,
}),
acl_management::UPDATE_ACL_RESULT,
30,
|c, url| {
c.patch(format!("{url}/acl/{}", encode_path_segment(did)))
.json(&req)
},
)
.await
}
pub async fn delete_acl(&self, did: &str) -> Result<(), VtaError> {
self.rpc_void(
acl_management::DELETE_ACL,
serde_json::json!({ "did": did }),
acl_management::DELETE_ACL_RESULT,
30,
|c, url| c.delete(format!("{url}/acl/{}", encode_path_segment(did))),
)
.await
}
/// Atomic self-service key rotation: move the caller's own ACL entry onto
/// the DID proven by `req.presentation` (a VP-JWT). Returns the new entry.
pub async fn swap_acl(&self, req: SwapAclRequest) -> Result<AclEntryResponse, VtaError> {
self.rpc(
acl_management::SWAP_ACL,
serde_json::to_value(&req)?,
acl_management::SWAP_ACL_RESULT,
30,
|c, url| c.post(format!("{url}/acl/swap")).json(&req),
)
.await
}
}