canton_admin/client.rs
1//! Ledger-API admin client: party management + user self-inspect.
2
3use std::sync::Arc;
4
5use canton_core::auth::{self, Intercepted};
6use canton_core::telemetry::{self, TRANSPORT_GRPC};
7use canton_core::{Config, Error, Result};
8use canton_proto::com::daml::ledger::api::v2 as lapi;
9use canton_proto::com::daml::ledger::api::v2::admin as pb;
10use lapi::package_service_client::PackageServiceClient;
11use pb::party_management_service_client::PartyManagementServiceClient;
12use pb::user_management_service_client::UserManagementServiceClient;
13use tonic::transport::Channel;
14
15/// A client for Canton's Ledger-API **admin** services: `PartyManagementService`
16/// and `UserManagementService`.
17///
18/// Served on the Ledger API port (e.g. `:3901`) — the same endpoint and auth as
19/// [`canton_ledger`]. Party-management RPCs require the `ParticipantAdmin`
20/// right; the `current_user*` self-inspect calls do not.
21///
22/// [`canton_ledger`]: https://docs.rs/canton-ledger
23#[derive(Clone, Debug)]
24pub struct AdminClient {
25 channel: Channel,
26 config: Arc<Config>,
27}
28
29/// Build a gRPC service client on the authenticated channel, with this
30/// client's decode limit applied.
31///
32/// A macro rather than a function because `max_decoding_message_size` is an
33/// inherent method on each generated client — tonic exposes no trait for it —
34/// so there is nothing to be generic over. Keeping every construction site
35/// behind one expansion is the point: `tonic`'s 4 MiB default is small enough
36/// that a real ACS page trips it, and a new RPC added later would otherwise
37/// pick the default up silently.
38macro_rules! service {
39 ($self:ident, $ctor:expr) => {
40 $ctor($self.intercepted().await?)
41 .max_decoding_message_size($self.config.max_decoding_message_size())
42 };
43}
44
45impl AdminClient {
46 /// Build a lazily-connected client. Returns immediately; the TCP/TLS
47 /// handshake happens on the first RPC.
48 ///
49 /// # Errors
50 /// Returns [`Error::InvalidRequest`] if the endpoint is not a valid URI.
51 pub fn connect_lazy(config: Config) -> Result<Self> {
52 Ok(Self {
53 channel: config.connect_channel()?,
54 config: Arc::new(config),
55 })
56 }
57
58 /// A channel wrapped with a fresh bearer-token interceptor for this call.
59 async fn intercepted(&self) -> Result<Intercepted> {
60 auth::intercepted(&self.channel, self.config.auth()).await
61 }
62
63 /// Run a unary operation under the configured retry policy (no-op when
64 /// retrying is not enabled).
65 async fn with_retry<T, F, Fut>(&self, op: F) -> Result<T>
66 where
67 F: FnMut() -> Fut,
68 Fut: std::future::Future<Output = Result<T>>,
69 {
70 canton_core::retry::run_with_retry(self.config.retry(), op).await
71 }
72
73 // ---- PartyManagementService --------------------------------------------
74
75 /// The participant's id (`GetParticipantId`). Works with any authenticated
76 /// token.
77 ///
78 /// # Errors
79 /// Returns an [`Error`] if the RPC fails.
80 pub async fn participant_id(&self) -> Result<String> {
81 telemetry::instrument("participant_id", TRANSPORT_GRPC, async {
82 self.with_retry(|| async {
83 let mut client = service!(self, PartyManagementServiceClient::new);
84 Ok(client
85 .get_participant_id(pb::GetParticipantIdRequest {})
86 .await?
87 .into_inner()
88 .participant_id)
89 })
90 .await
91 })
92 .await
93 }
94
95 /// Allocate a new party (`AllocateParty`). `party_id_hint` suggests the
96 /// local part of the party id; the participant returns the actual
97 /// [`PartyDetails`](pb::PartyDetails) (`party = "<hint>::<fingerprint>"`).
98 ///
99 /// Requires the `ParticipantAdmin` right. Not retried: party allocation is
100 /// a non-idempotent topology mutation (a lost response must not double
101 /// allocate), so a transient failure surfaces to the caller.
102 ///
103 /// # Errors
104 /// Returns an [`Error`] if authentication/authorization fails, the RPC
105 /// fails, or the response carries no party details.
106 pub async fn allocate_party(&self, party_id_hint: Option<&str>) -> Result<pb::PartyDetails> {
107 let party_id_hint = party_id_hint.unwrap_or_default().to_string();
108 telemetry::instrument("allocate_party", TRANSPORT_GRPC, async move {
109 let mut client = service!(self, PartyManagementServiceClient::new);
110 let response = client
111 .allocate_party(pb::AllocatePartyRequest {
112 party_id_hint,
113 ..Default::default()
114 })
115 .await?
116 .into_inner();
117 response.party_details.ok_or_else(|| {
118 Error::UnexpectedResponse("allocate_party returned no party details".to_string())
119 })
120 })
121 .await
122 }
123
124 /// List one page of known parties (`ListKnownParties`). Pass the returned
125 /// token to fetch the next page; `page_size` `0` uses the server default.
126 ///
127 /// # Errors
128 /// Returns an [`Error`] if authentication/authorization or the RPC fails.
129 pub async fn list_known_parties_page(
130 &self,
131 page_size: i32,
132 page_token: Option<String>,
133 ) -> Result<(Vec<pb::PartyDetails>, Option<String>)> {
134 telemetry::instrument("list_known_parties_page", TRANSPORT_GRPC, async move {
135 self.with_retry(|| {
136 let page_token = page_token.clone().unwrap_or_default();
137 async move {
138 let mut client = service!(self, PartyManagementServiceClient::new);
139 let response = client
140 .list_known_parties(pb::ListKnownPartiesRequest {
141 page_token,
142 page_size,
143 ..Default::default()
144 })
145 .await?
146 .into_inner();
147 let next =
148 (!response.next_page_token.is_empty()).then_some(response.next_page_token);
149 Ok((response.party_details, next))
150 }
151 })
152 .await
153 })
154 .await
155 }
156
157 /// All known parties, following pagination internally (`ListKnownParties`).
158 ///
159 /// # Errors
160 /// Returns an [`Error`] if authentication/authorization or any page RPC
161 /// fails.
162 pub async fn list_known_parties(&self) -> Result<Vec<pb::PartyDetails>> {
163 telemetry::instrument("list_known_parties", TRANSPORT_GRPC, async {
164 let mut all = Vec::new();
165 let mut page_token = String::new();
166 loop {
167 let sent = page_token.clone();
168 let (parties, next) = self
169 .with_retry(|| {
170 let page_token = sent.clone();
171 async move {
172 let mut client = service!(self, PartyManagementServiceClient::new);
173 let response = client
174 .list_known_parties(pb::ListKnownPartiesRequest {
175 page_token,
176 ..Default::default()
177 })
178 .await?
179 .into_inner();
180 Ok((response.party_details, response.next_page_token))
181 }
182 })
183 .await?;
184 all.extend(parties);
185 if next.is_empty() {
186 break;
187 }
188 // A server that never advances the token would loop forever.
189 // Stopping quietly is no better: the caller receives a prefix
190 // of the party list with nothing to say it is a prefix, and
191 // "which parties exist" is a question whose wrong answer looks
192 // exactly like a right one. So this fails.
193 if next == sent {
194 return Err(Error::UnexpectedResponse(format!(
195 "the participant repeated the same page token after {} parties; \
196 the list is incomplete and cannot be continued",
197 all.len()
198 )));
199 }
200 page_token = next;
201 }
202 Ok(all)
203 })
204 .await
205 }
206
207 /// Details for specific parties (`GetParties`). Unknown parties are silently
208 /// omitted from the result rather than erroring.
209 ///
210 /// # Errors
211 /// Returns an [`Error`] if authentication/authorization or the RPC fails.
212 pub async fn get_parties(&self, parties: Vec<String>) -> Result<Vec<pb::PartyDetails>> {
213 telemetry::instrument("get_parties", TRANSPORT_GRPC, async move {
214 self.with_retry(|| {
215 let parties = parties.clone();
216 async move {
217 let mut client = service!(self, PartyManagementServiceClient::new);
218 Ok(client
219 .get_parties(pb::GetPartiesRequest {
220 parties,
221 ..Default::default()
222 })
223 .await?
224 .into_inner()
225 .party_details)
226 }
227 })
228 .await
229 })
230 .await
231 }
232
233 // ---- UserManagementService (self-inspect) ------------------------------
234
235 /// The authenticated user's own record (`GetUser` with an empty user id).
236 /// Authorized for any authenticated token (no admin right needed).
237 ///
238 /// # Errors
239 /// Returns an [`Error`] if the RPC fails or the response carries no user.
240 pub async fn current_user(&self) -> Result<pb::User> {
241 self.get_user("").await
242 }
243
244 /// A user's record by id (`GetUser`); an empty `user_id` means the
245 /// authenticated user.
246 ///
247 /// # Errors
248 /// Returns an [`Error`] if the RPC fails or the response carries no user.
249 pub async fn get_user(&self, user_id: &str) -> Result<pb::User> {
250 let user_id = user_id.to_string();
251 telemetry::instrument("get_user", TRANSPORT_GRPC, async move {
252 self.with_retry(|| {
253 let user_id = user_id.clone();
254 async move {
255 let mut client = service!(self, UserManagementServiceClient::new);
256 let response = client
257 .get_user(pb::GetUserRequest {
258 user_id,
259 ..Default::default()
260 })
261 .await?
262 .into_inner();
263 response.user.ok_or_else(|| {
264 Error::UnexpectedResponse("get_user returned no user".to_string())
265 })
266 }
267 })
268 .await
269 })
270 .await
271 }
272
273 /// The authenticated user's own rights (`ListUserRights` with an empty user
274 /// id). Authorized for any authenticated token.
275 ///
276 /// # Errors
277 /// Returns an [`Error`] if the RPC fails.
278 pub async fn current_user_rights(&self) -> Result<Vec<pb::Right>> {
279 self.list_user_rights("").await
280 }
281
282 /// A user's rights by id (`ListUserRights`); an empty `user_id` means the
283 /// authenticated user.
284 ///
285 /// # Errors
286 /// Returns an [`Error`] if the RPC fails.
287 pub async fn list_user_rights(&self, user_id: &str) -> Result<Vec<pb::Right>> {
288 let user_id = user_id.to_string();
289 telemetry::instrument("list_user_rights", TRANSPORT_GRPC, async move {
290 self.with_retry(|| {
291 let user_id = user_id.clone();
292 async move {
293 let mut client = service!(self, UserManagementServiceClient::new);
294 Ok(client
295 .list_user_rights(pb::ListUserRightsRequest {
296 user_id,
297 ..Default::default()
298 })
299 .await?
300 .into_inner()
301 .rights)
302 }
303 })
304 .await
305 })
306 .await
307 }
308
309 // ---- PackageService (read) ----------------------------------------------
310
311 /// The ids of all Daml packages known to the participant
312 /// (`PackageService.ListPackages`).
313 ///
314 /// # Errors
315 /// Returns an [`Error`] if authentication or the RPC fails.
316 pub async fn list_packages(&self) -> Result<Vec<String>> {
317 telemetry::instrument("list_packages", TRANSPORT_GRPC, async {
318 self.with_retry(|| async {
319 let mut client = service!(self, PackageServiceClient::new);
320 Ok(client
321 .list_packages(lapi::ListPackagesRequest {})
322 .await?
323 .into_inner()
324 .package_ids)
325 })
326 .await
327 })
328 .await
329 }
330
331 /// The status of a package on the participant
332 /// (`PackageService.GetPackageStatus`): `Registered` if the package is
333 /// known, `Unspecified` otherwise.
334 ///
335 /// # Errors
336 /// Returns an [`Error`] if authentication or the RPC fails.
337 pub async fn get_package_status(&self, package_id: &str) -> Result<lapi::PackageStatus> {
338 let package_id = package_id.to_string();
339 telemetry::instrument("get_package_status", TRANSPORT_GRPC, async move {
340 self.with_retry(|| {
341 let package_id = package_id.clone();
342 async move {
343 let mut client = service!(self, PackageServiceClient::new);
344 let response = client
345 .get_package_status(lapi::GetPackageStatusRequest { package_id })
346 .await?
347 .into_inner();
348 Ok(response.package_status())
349 }
350 })
351 .await
352 })
353 .await
354 }
355
356 /// The specific parties the authenticated user may act as — the `CanActAs`
357 /// subset of [`Self::current_user_rights`]. Supersedes the ad-hoc party
358 /// discovery used in early tests.
359 ///
360 /// Note: a user granted the wildcard `CanActAsAnyParty` right may act as
361 /// *any* party yet carries no explicit `CanActAs` entries, so this returns
362 /// an empty list for such users — an empty result means "no specific
363 /// parties", not necessarily "cannot act". Inspect
364 /// [`Self::current_user_rights`] directly if you need the wildcard.
365 ///
366 /// # Errors
367 /// Returns an [`Error`] if the underlying rights RPC fails.
368 pub async fn acting_parties(&self) -> Result<Vec<String>> {
369 let rights = self.current_user_rights().await?;
370 Ok(rights
371 .into_iter()
372 .filter_map(|right| match right.kind {
373 Some(pb::right::Kind::CanActAs(can)) => Some(can.party),
374 _ => None,
375 })
376 .collect())
377 }
378}