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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! ACL methods on [`VtaClient`].
use super::types::AclEntryEnvelope;
use super::{
AclEntryResponse, AclListResponse, ChangeAclRoleRequest, 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> {
// Canonical `acl/show/0.1` wraps the entry (alongside `redactedFields`);
// unwrap so callers keep working with the entry itself.
let wrapped: AclEntryEnvelope = self
.rpc(
acl_management::GET_ACL,
serde_json::json!({ "subject": did }),
acl_management::GET_ACL_RESULT,
30,
|c, url| c.get(format!("{url}/acl/{}", encode_path_segment(did))),
)
.await?;
Ok(wrapped.entry)
}
pub async fn create_acl(&self, req: CreateAclRequest) -> Result<AclEntryResponse, VtaError> {
// Canonical `acl/grant/0.1` returns the realized entry under `entry`.
let wrapped: AclEntryEnvelope = 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?;
Ok(wrapped.entry)
}
pub async fn update_acl(
&self,
did: &str,
req: UpdateAclRequest,
) -> Result<AclEntryResponse, VtaError> {
// Canonical `acl/update/0.1` returns the realized entry under `entry`,
// like grant and show.
let wrapped: AclEntryEnvelope = self
.rpc(
acl_management::UPDATE_ACL,
serde_json::json!({
"subject": did,
"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?;
Ok(wrapped.entry)
}
/// Transition a subject's role, compare-and-swapped against the role
/// they currently hold.
///
/// Separate from [`Self::update_acl`] because role is the one ACL
/// attribute where a lost update is a privilege change. If another
/// admin has moved the subject since you read them, this fails
/// rather than overwriting their change — re-read and retry.
pub async fn change_acl_role(
&self,
did: &str,
req: ChangeAclRoleRequest,
) -> Result<AclEntryResponse, VtaError> {
let wrapped: AclEntryEnvelope = self
.rpc(
acl_management::CHANGE_ROLE,
serde_json::json!({
"subject": did,
"fromRole": &req.from_role,
"toRole": &req.to_role,
"reason": &req.reason,
}),
acl_management::CHANGE_ROLE_RESULT,
30,
|c, url| {
c.post(format!(
"{url}/acl/{}/change-role",
encode_path_segment(did)
))
.json(&req)
},
)
.await?;
Ok(wrapped.entry)
}
pub async fn delete_acl(&self, did: &str) -> Result<(), VtaError> {
self.rpc_void(
acl_management::DELETE_ACL,
serde_json::json!({ "subject": 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
}
}