Skip to main content

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}