Skip to main content

aranya_client/client/
team.rs

1use anyhow::Context as _;
2use aranya_crypto::EncryptionPublicKey;
3use aranya_daemon_api::{self as api, CS};
4use aranya_id::custom_id;
5use aranya_policy_text::Text;
6use aranya_util::Addr;
7use buggy::BugExt as _;
8use tracing::instrument;
9
10#[cfg(feature = "preview")]
11use crate::client::{Permission, RoleManagementPermission};
12use crate::{
13    client::{
14        create_ctx, Client, Device, DeviceId, Devices, Label, LabelId, Labels, PublicKeyBundle,
15        Role, RoleId, Roles,
16    },
17    config::SyncPeerConfig,
18    error::{self, aranya_error, IpcError, Result},
19    util::{ApiConv as _, ApiId},
20};
21
22custom_id! {
23    /// Uniquely identifies an Aranya team.
24    pub struct TeamId;
25}
26impl ApiId<api::TeamId> for TeamId {}
27
28/// Represents an Aranya Team.
29#[derive(Debug)]
30pub struct Team<'a> {
31    pub(super) client: &'a Client,
32    pub(super) id: api::TeamId,
33}
34
35impl Team<'_> {
36    /// Return the team's globally unique ID.
37    pub fn team_id(&self) -> TeamId {
38        TeamId::from_api(self.id)
39    }
40
41    /// Closes the team, preventing any further operations on it.
42    #[instrument(skip(self))]
43    pub async fn close_team(&self) -> Result<()> {
44        self.client
45            .daemon
46            .close_team(create_ctx(), self.id)
47            .await
48            .map_err(IpcError::new)?
49            .map_err(aranya_error)
50    }
51}
52
53impl Team<'_> {
54    /// Encrypts the team's QUIC syncer PSK seed for a peer.
55    /// `peer_enc_pk` is the public encryption key of the peer device.
56    /// See [`PublicKeyBundle::encryption`].
57    #[instrument(skip(self))]
58    pub async fn encrypt_psk_seed_for_peer(&self, peer_enc_pk: &[u8]) -> Result<Vec<u8>> {
59        let peer_enc_pk: EncryptionPublicKey<CS> = postcard::from_bytes(peer_enc_pk)
60            .context("bad peer_enc_pk")
61            .map_err(error::other)?;
62        let wrapped = self
63            .client
64            .daemon
65            .encrypt_psk_seed_for_peer(create_ctx(), self.id, peer_enc_pk)
66            .await
67            .map_err(IpcError::new)?
68            .map_err(aranya_error)?;
69        let wrapped = postcard::to_allocvec(&wrapped).assume("can serialize")?;
70        Ok(wrapped)
71    }
72
73    /// Adds a peer for automatic periodic Aranya state syncing.
74    #[instrument(skip(self))]
75    pub async fn add_sync_peer(&self, addr: Addr, config: SyncPeerConfig) -> Result<()> {
76        self.client
77            .daemon
78            .add_sync_peer(create_ctx(), addr, self.id, config.into())
79            .await
80            .map_err(IpcError::new)?
81            .map_err(aranya_error)
82    }
83
84    /// Immediately syncs with the peer.
85    ///
86    /// If `config` is `None`, default values (including those from the daemon) will
87    /// be used.
88    #[instrument(skip(self))]
89    pub async fn sync_now(&self, addr: Addr, cfg: Option<SyncPeerConfig>) -> Result<()> {
90        self.client
91            .daemon
92            .sync_now(create_ctx(), addr, self.id, cfg.map(Into::into))
93            .await
94            .map_err(IpcError::new)?
95            .map_err(aranya_error)
96    }
97
98    /// Removes a peer from automatic Aranya state syncing.
99    #[instrument(skip(self))]
100    pub async fn remove_sync_peer(&self, addr: Addr) -> Result<()> {
101        self.client
102            .daemon
103            .remove_sync_peer(create_ctx(), addr, self.id)
104            .await
105            .map_err(IpcError::new)?
106            .map_err(aranya_error)
107    }
108}
109
110impl Team<'_> {
111    /// Adds a device to the team with an optional initial role.
112    #[instrument(skip(self))]
113    pub async fn add_device(
114        &self,
115        keys: PublicKeyBundle,
116        initial_role: Option<RoleId>,
117    ) -> Result<()> {
118        self.client
119            .daemon
120            .add_device_to_team(
121                create_ctx(),
122                self.id,
123                keys.into_api(),
124                initial_role.map(RoleId::into_api),
125            )
126            .await
127            .map_err(IpcError::new)?
128            .map_err(aranya_error)
129    }
130
131    /// Returns the [`Device`] corresponding with `id`.
132    pub fn device(&self, id: DeviceId) -> Device<'_> {
133        Device {
134            client: self.client,
135            team_id: self.id,
136            id: id.into_api(),
137        }
138    }
139
140    /// Returns the list of devices on the team.
141    #[instrument(skip(self))]
142    pub async fn devices(&self) -> Result<Devices> {
143        let data = self
144            .client
145            .daemon
146            .devices_on_team(create_ctx(), self.id)
147            .await
148            .map_err(IpcError::new)?
149            .map_err(aranya_error)?
150            // This _should_ just be `into_iter`, but the
151            // compiler chooses the `&Box` impl. It's the same
152            // end result, though.
153            .into_vec()
154            .into_iter()
155            .map(DeviceId::from_api)
156            .collect();
157        Ok(Devices { data })
158    }
159
160    /// Subscribe to hello notifications from a sync peer.
161    ///
162    /// This will request the peer to send hello notifications when their graph head changes.
163    ///
164    /// # Parameters
165    ///
166    /// * `peer` - The address of the sync peer to subscribe to.
167    /// * `config` - Configuration for the hello subscription including delays and expiration.
168    ///
169    /// To automatically sync when receiving a hello message, call [`Self::add_sync_peer`] with
170    /// [`crate::config::SyncPeerConfigBuilder::sync_on_hello`] set to `true`.
171    #[cfg(feature = "preview")]
172    #[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
173    pub async fn sync_hello_subscribe(
174        &self,
175        peer: Addr,
176        config: crate::config::HelloSubscriptionConfig,
177    ) -> Result<()> {
178        // TODO(#709): Pass the config type directly into the daemon IPC and internal
179        // daemon implementation instead of extracting individual fields here.
180        self.client
181            .daemon
182            .sync_hello_subscribe(
183                create_ctx(),
184                peer,
185                self.id,
186                config.graph_change_debounce(),
187                config.expiration(),
188                config.periodic_interval(),
189            )
190            .await
191            .map_err(IpcError::new)?
192            .map_err(aranya_error)
193    }
194
195    /// Unsubscribe from hello notifications from a sync peer.
196    ///
197    /// This will stop receiving hello notifications from the specified peer.
198    #[cfg(feature = "preview")]
199    #[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
200    pub async fn sync_hello_unsubscribe(&self, peer: Addr) -> Result<()> {
201        self.client
202            .daemon
203            .sync_hello_unsubscribe(create_ctx(), peer, self.id)
204            .await
205            .map_err(IpcError::new)?
206            .map_err(aranya_error)
207    }
208}
209
210impl Team<'_> {
211    /// Sets up the default team roles.
212    ///
213    /// `owning_role` will be the initial owner of the default
214    /// roles.
215    ///
216    /// It returns the the roles that were created.
217    #[instrument(skip(self))]
218    pub async fn setup_default_roles(&self, owning_role: RoleId) -> Result<Roles> {
219        let roles = self
220            .client
221            .daemon
222            .setup_default_roles(create_ctx(), self.id, owning_role.into_api())
223            .await
224            .map_err(IpcError::new)?
225            .map_err(aranya_error)?
226            // This _should_ just be `into_iter`, but the
227            // compiler chooses the `&Box` impl. It's the same
228            // end result, though.
229            .into_vec()
230            .into_iter()
231            .map(Role::from_api)
232            .collect();
233        Ok(Roles { roles })
234    }
235
236    /// Creates a new role.
237    ///
238    /// `owning_role` will be the initial owner of the new role.
239    ///
240    /// It returns the Role that was created.
241    #[cfg(feature = "preview")]
242    #[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
243    #[instrument(skip(self))]
244    pub async fn create_role(&self, role_name: Text, owning_role: RoleId) -> Result<Role> {
245        let role = self
246            .client
247            .daemon
248            .create_role(create_ctx(), self.id, role_name, owning_role.into_api())
249            .await
250            .map_err(IpcError::new)?
251            .map_err(aranya_error)?;
252        Ok(Role::from_api(role))
253    }
254
255    /// Deletes a role.
256    ///
257    /// The role must not be assigned to any devices, nor should it own
258    /// any other roles.
259    #[cfg(feature = "preview")]
260    #[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
261    #[instrument(skip(self))]
262    pub async fn delete_role(&self, role_id: RoleId) -> Result<()> {
263        self.client
264            .daemon
265            .delete_role(create_ctx(), self.id, role_id.into_api())
266            .await
267            .map_err(IpcError::new)?
268            .map_err(aranya_error)?;
269        Ok(())
270    }
271
272    /// Adds a permission to a role.
273    #[cfg(feature = "preview")]
274    #[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
275    #[instrument(skip(self))]
276    pub async fn add_perm_to_role(&self, role_id: RoleId, perm: Permission) -> Result<()> {
277        self.client
278            .daemon
279            .add_perm_to_role(create_ctx(), self.id, role_id.into_api(), perm)
280            .await
281            .map_err(IpcError::new)?
282            .map_err(aranya_error)?;
283        Ok(())
284    }
285
286    /// Removes a permission from a role.
287    #[cfg(feature = "preview")]
288    #[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
289    #[instrument(skip(self))]
290    pub async fn remove_perm_from_role(&self, role_id: RoleId, perm: Permission) -> Result<()> {
291        self.client
292            .daemon
293            .remove_perm_from_role(create_ctx(), self.id, role_id.into_api(), perm)
294            .await
295            .map_err(IpcError::new)?
296            .map_err(aranya_error)?;
297        Ok(())
298    }
299
300    /// Adds `owning_role` as an owner of `role`.
301    #[cfg(feature = "preview")]
302    #[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
303    #[instrument(skip(self))]
304    pub async fn add_role_owner(&self, role: RoleId, owning_role: RoleId) -> Result<()> {
305        self.client
306            .daemon
307            .add_role_owner(
308                create_ctx(),
309                self.id,
310                role.into_api(),
311                owning_role.into_api(),
312            )
313            .await
314            .map_err(IpcError::new)?
315            .map_err(aranya_error)
316    }
317
318    /// Removes an `owning_role` as an owner of `role`.
319    #[cfg(feature = "preview")]
320    #[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
321    #[instrument(skip(self))]
322    pub async fn remove_role_owner(&self, role: RoleId, owning_role: RoleId) -> Result<()> {
323        self.client
324            .daemon
325            .remove_role_owner(
326                create_ctx(),
327                self.id,
328                role.into_api(),
329                owning_role.into_api(),
330            )
331            .await
332            .map_err(IpcError::new)?
333            .map_err(aranya_error)
334    }
335
336    /// Returns the roles that own `role`.
337    #[instrument(skip(self))]
338    pub async fn role_owners(&self, role: RoleId) -> Result<Roles> {
339        let roles = self
340            .client
341            .daemon
342            .role_owners(create_ctx(), self.id, role.into_api())
343            .await
344            .map_err(IpcError::new)?
345            .map_err(aranya_error)?
346            // This _should_ just be `into_iter`, but the
347            // compiler chooses the `&Box` impl. It's the same
348            // end result, though.
349            .into_vec()
350            .into_iter()
351            .map(Role::from_api)
352            .collect();
353        Ok(Roles { roles })
354    }
355
356    /// Assigns a role management permission to a managing role.
357    #[cfg(feature = "preview")]
358    #[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
359    #[instrument(skip(self))]
360    pub async fn assign_role_management_permission(
361        &self,
362        role: RoleId,
363        managing_role: RoleId,
364        perm: RoleManagementPermission,
365    ) -> Result<()> {
366        self.client
367            .daemon
368            .assign_role_management_perm(
369                create_ctx(),
370                self.id,
371                role.into_api(),
372                managing_role.into_api(),
373                perm,
374            )
375            .await
376            .map_err(IpcError::new)?
377            .map_err(aranya_error)
378    }
379
380    /// Revokes a role management permission from a managing
381    /// role.
382    #[cfg(feature = "preview")]
383    #[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
384    #[instrument(skip(self))]
385    pub async fn revoke_role_management_permission(
386        &self,
387        role: RoleId,
388        managing_role: RoleId,
389        perm: RoleManagementPermission,
390    ) -> Result<()> {
391        self.client
392            .daemon
393            .revoke_role_management_perm(
394                create_ctx(),
395                self.id,
396                role.into_api(),
397                managing_role.into_api(),
398                perm,
399            )
400            .await
401            .map_err(IpcError::new)?
402            .map_err(aranya_error)
403    }
404
405    /// Returns all of the roles for this team.
406    #[instrument(skip(self))]
407    pub async fn roles(&self) -> Result<Roles> {
408        let roles = self
409            .client
410            .daemon
411            .team_roles(create_ctx(), self.id)
412            .await
413            .map_err(IpcError::new)?
414            .map_err(aranya_error)?
415            // This _should_ just be `into_iter`, but the
416            // compiler chooses the `&Box` impl. It's the same
417            // end result, though.
418            .into_vec()
419            .into_iter()
420            .map(Role::from_api)
421            .collect();
422        Ok(Roles { roles })
423    }
424}
425
426impl Team<'_> {
427    /// Create a label.
428    #[instrument(skip(self))]
429    pub async fn create_label(
430        &self,
431        label_name: Text,
432        managing_role_id: RoleId,
433    ) -> Result<LabelId> {
434        self.client
435            .daemon
436            .create_label(
437                create_ctx(),
438                self.id,
439                label_name,
440                managing_role_id.into_api(),
441            )
442            .await
443            .map_err(IpcError::new)?
444            .map_err(aranya_error)
445            .map(LabelId::from_api)
446    }
447
448    /// Delete a label.
449    #[instrument(skip(self))]
450    pub async fn delete_label(&self, label_id: LabelId) -> Result<()> {
451        self.client
452            .daemon
453            .delete_label(create_ctx(), self.id, label_id.into_api())
454            .await
455            .map_err(IpcError::new)?
456            .map_err(aranya_error)
457    }
458
459    /// Add label managing role
460    #[instrument(skip(self))]
461    pub async fn add_label_managing_role(
462        &self,
463        label_id: LabelId,
464        managing_role_id: RoleId,
465    ) -> Result<()> {
466        self.client
467            .daemon
468            .add_label_managing_role(
469                create_ctx(),
470                self.id,
471                label_id.into_api(),
472                managing_role_id.into_api(),
473            )
474            .await
475            .map_err(IpcError::new)?
476            .map_err(aranya_error)
477    }
478
479    /// Returns a label if it exists.
480    #[instrument(skip(self))]
481    pub async fn label(&self, label_id: LabelId) -> Result<Option<Label>> {
482        let label = self
483            .client
484            .daemon
485            .label(create_ctx(), self.id, label_id.into_api())
486            .await
487            .map_err(IpcError::new)?
488            .map_err(aranya_error)?
489            .map(Label::from_api);
490        Ok(label)
491    }
492
493    /// Returns the list of labels on the team.
494    #[instrument(skip(self))]
495    pub async fn labels(&self) -> Result<Labels> {
496        let labels = self
497            .client
498            .daemon
499            .labels(create_ctx(), self.id)
500            .await
501            .map_err(IpcError::new)?
502            .map_err(aranya_error)?
503            .into_iter()
504            .map(Label::from_api)
505            .collect();
506        Ok(Labels { labels })
507    }
508}