Skip to main content

aranya_daemon/
api.rs

1//! Implementation of daemon's `tarpc` API.
2//! Trait for API interface is defined in `crates/aranya-daemon-api`
3
4#![allow(clippy::expect_used, clippy::panic, clippy::indexing_slicing)]
5
6use core::{future, net::SocketAddr, ops::Deref, pin::pin};
7#[cfg(feature = "preview")]
8use std::collections::HashMap;
9#[cfg(feature = "preview")]
10use std::time::Duration;
11use std::{path::PathBuf, sync::Arc};
12
13use anyhow::{anyhow, Context as _};
14use aranya_crypto::{
15    default::WrappedKey,
16    policy::{GroupId, LabelId, RoleId},
17    Csprng, DeviceId, EncryptionKey, EncryptionPublicKey, KeyStore as _, KeyStoreExt as _, Rng,
18};
19pub(crate) use aranya_daemon_api::crypto::ApiKey;
20use aranya_daemon_api::{
21    self as api,
22    crypto::txp::{self, LengthDelimitedCodec},
23    DaemonApi, Text, WrappedSeed,
24};
25use aranya_keygen::PublicKeys;
26use aranya_runtime::GraphId;
27#[cfg(feature = "preview")]
28use aranya_runtime::{Address, Storage, StorageProvider};
29use aranya_util::{error::ReportExt as _, ready, task::scope, Addr};
30#[cfg(feature = "afc")]
31use buggy::bug;
32use derive_where::derive_where;
33use futures_util::{StreamExt, TryStreamExt};
34pub(crate) use quic_sync::Data as QSData;
35use tarpc::{
36    context,
37    server::{incoming::Incoming, BaseChannel, Channel},
38};
39use tokio::{
40    net::UnixListener,
41    sync::{mpsc, Mutex},
42};
43use tracing::{debug, error, info, instrument, trace, warn};
44
45#[cfg(feature = "afc")]
46use crate::actions::SessionData;
47#[cfg(feature = "afc")]
48use crate::afc::Afc;
49use crate::{
50    actions::Actions,
51    daemon::{CE, CS, KS},
52    keystore::LocalStore,
53    policy::{ChanOp, Effect, PublicKeyBundle, RoleCreated, RoleManagementPerm, SimplePerm},
54    sync::{quic as qs, SyncHandle, SyncPeer},
55    util::SeedDir,
56    AranyaStore, Client, EF,
57};
58
59mod quic_sync;
60
61/// Find the first effect matching a given pattern.
62///
63/// Returns `None` if there are no matching effects.
64#[macro_export]
65macro_rules! find_effect {
66    ($effects:expr, $pattern:pat $(if $guard:expr)? $(,)?) => {
67        $effects.into_iter().find(|e| matches!(e, $pattern $(if $guard)?))
68    }
69}
70
71/// Daemon API Server.
72#[derive(Debug)]
73pub(crate) struct DaemonApiServer {
74    /// Used to encrypt data sent over the API.
75    sk: ApiKey<CS>,
76    /// The UDS path we serve the API on.
77    uds_path: PathBuf,
78    /// Socket bound to `uds_path`.
79    listener: UnixListener,
80
81    /// Channel for receiving effects from the syncer.
82    recv_effects: mpsc::Receiver<(GraphId, Vec<EF>)>,
83
84    /// Api Handler.
85    api: Api,
86}
87
88pub(crate) struct DaemonApiServerArgs {
89    pub(crate) client: Client,
90    pub(crate) local_addr: SocketAddr,
91    pub(crate) uds_path: PathBuf,
92    pub(crate) sk: ApiKey<CS>,
93    pub(crate) pk: PublicKeys<CS>,
94    pub(crate) syncer: SyncHandle,
95    pub(crate) recv_effects: mpsc::Receiver<(GraphId, Vec<EF>)>,
96    #[cfg(feature = "afc")]
97    pub(crate) afc: Afc<CE, CS, KS>,
98    pub(crate) crypto: Crypto,
99    pub(crate) seed_id_dir: SeedDir,
100    pub(crate) quic: Option<quic_sync::Data>,
101}
102
103impl DaemonApiServer {
104    /// Creates a `DaemonApiServer`.
105    #[instrument(skip_all)]
106    pub(crate) fn new(
107        DaemonApiServerArgs {
108            client,
109            local_addr,
110            uds_path,
111            sk,
112            pk,
113            syncer,
114            recv_effects,
115            #[cfg(feature = "afc")]
116            afc,
117            crypto,
118            seed_id_dir,
119            quic,
120        }: DaemonApiServerArgs,
121    ) -> anyhow::Result<Self> {
122        let listener = UnixListener::bind(&uds_path)?;
123        let uds_path = uds_path
124            .canonicalize()
125            .context("could not canonicalize uds_path")?;
126        #[cfg(feature = "afc")]
127        let afc = Arc::new(afc);
128        let effect_handler = EffectHandler {
129            #[cfg(feature = "afc")]
130            afc: afc.clone(),
131            #[cfg(feature = "afc")]
132            device_id: pk.ident_pk.id()?,
133            #[cfg(feature = "preview")]
134            client: client.clone(),
135            #[cfg(feature = "preview")]
136            syncer: syncer.clone(),
137            #[cfg(feature = "preview")]
138            prev_head_addresses: Arc::default(),
139        };
140        let api = Api(Arc::new(ApiInner {
141            client,
142            local_addr,
143            pk: std::sync::Mutex::new(pk),
144            syncer,
145            effect_handler,
146            #[cfg(feature = "afc")]
147            afc,
148            crypto: Mutex::new(crypto),
149            seed_id_dir,
150            quic,
151        }));
152        Ok(Self {
153            uds_path,
154            sk,
155            recv_effects,
156            listener,
157            api,
158        })
159    }
160
161    /// Runs the server.
162    pub(crate) async fn serve(mut self, ready: ready::Notifier) {
163        scope(async |s| {
164            s.spawn({
165                let effect_handler = self.api.effect_handler.clone();
166                async move {
167                    while let Some((graph, effects)) = self.recv_effects.recv().await {
168                        if let Err(err) = effect_handler.handle_effects(graph, &effects).await {
169                            error!(error = ?err, "error handling effects");
170                        }
171                    }
172                    info!("effect handler exiting");
173                }
174            });
175
176            let server = {
177                let info = self.uds_path.as_os_str().as_encoded_bytes();
178                let codec = LengthDelimitedCodec::builder()
179                    .max_frame_length(usize::MAX)
180                    .new_codec();
181                let listener = txp::unix::UnixListenerStream::from(self.listener);
182                txp::server(listener, codec, self.sk, info)
183            };
184            info!(path = ?self.uds_path, "listening");
185
186            let mut incoming = server
187                .inspect_err(|err| warn!(error = %err.report(), "accept error"))
188                .filter_map(|r| future::ready(r.ok()))
189                .map(BaseChannel::with_defaults)
190                .max_concurrent_requests_per_channel(10);
191
192            ready.notify();
193
194            while let Some(ch) = incoming.next().await {
195                let api = self.api.clone();
196                s.spawn(scope(async move |reqs| {
197                    let requests = ch
198                        .requests()
199                        .inspect_err(|err| warn!(error = %err.report(), "channel failure"))
200                        .take_while(|r| future::ready(r.is_ok()))
201                        .filter_map(|r| async { r.ok() });
202                    let mut requests = pin!(requests);
203                    while let Some(req) = requests.next().await {
204                        reqs.spawn(req.execute(api.clone().serve()));
205                    }
206                }));
207            }
208        })
209        .await;
210
211        info!("server exiting");
212    }
213}
214
215/// Handles effects from an Aranya action.
216#[derive(Clone, Debug)]
217struct EffectHandler {
218    #[cfg(feature = "afc")]
219    afc: Arc<Afc<CE, CS, KS>>,
220    #[cfg(feature = "afc")]
221    device_id: DeviceId,
222    #[cfg(feature = "preview")]
223    client: Client,
224    #[cfg(feature = "preview")]
225    syncer: SyncHandle,
226    /// Stores the previous head address for each graph to detect changes
227    #[cfg(feature = "preview")]
228    prev_head_addresses: Arc<Mutex<HashMap<GraphId, Address>>>,
229}
230
231impl EffectHandler {
232    /// Handles effects resulting from invoking an Aranya action.
233    #[instrument(skip_all, fields(%graph, effects = effects.len()))]
234    async fn handle_effects(&self, graph: GraphId, effects: &[Effect]) -> anyhow::Result<()> {
235        trace!("handling effects");
236
237        use Effect::*;
238        // TODO: support feature flag in interface generator to compile out certain effects.
239        for effect in effects {
240            trace!(?effect, "handling effect");
241            match effect {
242                TeamCreated(_) => {}
243                TeamTerminated(_) => {}
244                DeviceAdded(_) => {}
245                DeviceRemoved(_) => {}
246                RoleAssigned(_) => {}
247                RoleRevoked(_) => {}
248                LabelCreated(_) => {}
249                LabelDeleted(_) => {}
250                AssignedLabelToDevice(_) => {}
251                LabelRevokedFromDevice(_) => {}
252                QueryLabelResult(_) => {}
253                AfcUniChannelCreated(_) => {}
254                AfcUniChannelReceived(_) => {}
255                QueryDevicesOnTeamResult(_) => {}
256                QueryDeviceRoleResult(_) => {}
257                QueryDeviceKeyBundleResult(_) => {}
258                QueryLabelsAssignedToDeviceResult(_) => {}
259                LabelManagingRoleAdded(_) => {}
260                LabelManagingRoleRevoked(_) => {}
261                PermAddedToRole(_) => {}
262                PermRemovedFromRole(_) => {}
263                RoleOwnerAdded(_) => {}
264                RoleOwnerRemoved(_) => {}
265                RoleManagementPermAssigned(_) => {}
266                RoleManagementPermRevoked(_) => {}
267                RoleChanged(_) => {}
268                QueryLabelsResult(_) => {}
269                QueryTeamRolesResult(_) => {}
270                QueryRoleOwnersResult(_) => {}
271                QueryAfcChannelIsValidResult(_) => {}
272                RoleCreated(_) => {}
273                RoleDeleted(_) => {}
274                CheckValidAfcChannels(_) => {
275                    #[cfg(feature = "afc")]
276                    self.afc
277                        .remove_invalid_channels(graph, self.device_id)
278                        .await?;
279                }
280            }
281        }
282
283        #[cfg(feature = "preview")]
284        {
285            // Check if the graph head address has changed
286            let Some(current_head) = self.get_graph_head_address(graph).await else {
287                warn!(?graph, "unable to get current graph head address");
288                return Ok(());
289            };
290
291            let mut prev_addresses = self.prev_head_addresses.lock().await;
292            let has_graph_changes = match prev_addresses.get(&graph) {
293                Some(prev_head) => prev_head != &current_head,
294                None => true, // First time seeing this graph
295            };
296
297            if has_graph_changes {
298                trace!(
299                    ?graph,
300                    ?current_head,
301                    "graph head address changed, triggering hello notification broadcast"
302                );
303                // Update stored head address
304                HashMap::insert(&mut prev_addresses, graph, current_head);
305                drop(prev_addresses); // Release the lock before async call
306
307                self.broadcast_hello_notifications(graph, current_head)
308                    .await;
309            } else {
310                trace!(
311                    ?graph,
312                    "graph head address unchanged, no hello broadcast needed"
313                );
314            }
315        }
316
317        Ok(())
318    }
319
320    /// Gets the current graph head address using the proper Location->Segment->Command->Address flow.
321    #[cfg(feature = "preview")]
322    async fn get_graph_head_address(&self, graph_id: GraphId) -> Option<Address> {
323        let client = &self.client;
324
325        let mut aranya = client.lock_aranya().await;
326        let storage = aranya.provider().get_storage(graph_id).ok()?;
327
328        storage.get_head_address().ok()
329    }
330
331    /// Broadcasts hello notifications to subscribers when the graph changes.
332    #[cfg(feature = "preview")]
333    #[instrument(skip(self))]
334    async fn broadcast_hello_notifications(&self, graph_id: GraphId, head: Address) {
335        // TODO: Don't fire off a spawn here.
336        let syncer = self.syncer.clone();
337        drop(tokio::spawn(async move {
338            if let Err(e) = syncer.broadcast_hello(graph_id, head).await {
339                warn!(
340                    error = %e,
341                    ?graph_id,
342                    ?head,
343                    "peers.broadcast_hello failed"
344                );
345            }
346        }));
347    }
348}
349
350/// The guts of [`Api`].
351///
352/// This is separated out so we only have to clone one [`Arc`]
353/// (inside [`Api`]).
354#[derive_where(Debug)]
355struct ApiInner {
356    client: Client,
357    /// Local socket address of the API.
358    local_addr: SocketAddr,
359    /// Public keys of current device.
360    pk: std::sync::Mutex<PublicKeys<CS>>,
361    /// Handle to talk with the syncer.
362    syncer: SyncHandle,
363    /// Handles graph effects from the syncer.
364    #[derive_where(skip(Debug))]
365    effect_handler: EffectHandler,
366    #[cfg(feature = "afc")]
367    afc: Arc<Afc<CE, CS, KS>>,
368    #[derive_where(skip(Debug))]
369    crypto: Mutex<Crypto>,
370    seed_id_dir: SeedDir,
371    quic: Option<quic_sync::Data>,
372}
373
374pub(crate) struct Crypto {
375    pub(crate) engine: CE,
376    pub(crate) local_store: LocalStore<KS>,
377    pub(crate) aranya_store: AranyaStore<KS>,
378}
379
380impl ApiInner {
381    fn get_pk(&self) -> api::Result<PublicKeyBundle> {
382        let pk = self.pk.lock().expect("poisoned");
383        Ok(PublicKeyBundle::try_from(&*pk).context("bad key bundle")?)
384    }
385
386    fn device_id(&self) -> api::Result<DeviceId> {
387        let pk = self.pk.lock().expect("poisoned");
388        let id = pk.ident_pk.id()?;
389        Ok(id)
390    }
391}
392
393/// Implements [`DaemonApi`].
394#[derive(Clone, Debug)]
395struct Api(Arc<ApiInner>);
396
397impl Deref for Api {
398    type Target = ApiInner;
399
400    fn deref(&self) -> &Self::Target {
401        &self.0
402    }
403}
404
405impl Api {
406    /// Checks wither a team's graph is valid.
407    /// If the graph is not valid, return an error to prevent operations on the invalid graph.
408    async fn check_team_valid(&self, team: api::TeamId) -> anyhow::Result<GraphId> {
409        if self
410            .client
411            .invalid_graphs()
412            .contains(GraphId::transmute(team))
413        {
414            // TODO: return custom daemon error type
415            anyhow::bail!("team {team} invalid due to graph finalization error")
416        }
417        Ok(GraphId::transmute(team))
418    }
419}
420
421impl DaemonApi for Api {
422    //
423    // Misc
424    //
425
426    #[instrument(skip(self), err)]
427    async fn version(self, context: context::Context) -> api::Result<api::Version> {
428        api::Version::parse(env!("CARGO_PKG_VERSION")).map_err(Into::into)
429    }
430
431    #[instrument(skip(self), err)]
432    async fn aranya_local_addr(self, context: context::Context) -> api::Result<Addr> {
433        Ok(self.local_addr.into())
434    }
435
436    #[instrument(skip(self), err)]
437    async fn get_public_key_bundle(self, _: context::Context) -> api::Result<api::PublicKeyBundle> {
438        Ok(self
439            .get_pk()
440            .context("unable to get device public keys")?
441            .into())
442    }
443
444    #[instrument(skip(self), err)]
445    async fn get_device_id(self, _: context::Context) -> api::Result<api::DeviceId> {
446        self.device_id().map(api::DeviceId::transmute)
447    }
448
449    #[cfg(feature = "afc")]
450    #[instrument(skip(self), err)]
451    async fn afc_shm_info(self, context: context::Context) -> api::Result<api::AfcShmInfo> {
452        Ok(self.afc.get_shm_info().await)
453    }
454
455    //
456    // Syncing
457    //
458
459    #[instrument(skip(self), err)]
460    async fn add_sync_peer(
461        self,
462        _: context::Context,
463        peer: Addr,
464        team: api::TeamId,
465        cfg: api::SyncPeerConfig,
466    ) -> api::Result<()> {
467        let graph = self.check_team_valid(team).await?;
468        let peer = SyncPeer::new(peer, graph);
469        self.syncer.add_peer(peer, cfg).await?;
470        Ok(())
471    }
472
473    #[instrument(skip(self), err)]
474    async fn sync_now(
475        self,
476        _: context::Context,
477        peer: Addr,
478        team: api::TeamId,
479        cfg: Option<api::SyncPeerConfig>,
480    ) -> api::Result<()> {
481        let graph = self.check_team_valid(team).await?;
482        let peer = SyncPeer::new(peer, graph);
483        self.syncer.sync_now(peer, cfg).await?;
484        Ok(())
485    }
486
487    #[cfg(feature = "preview")]
488    #[instrument(skip(self), err)]
489    async fn sync_hello_subscribe(
490        self,
491        _: context::Context,
492        peer: Addr,
493        team: api::TeamId,
494        graph_change_debounce: Duration,
495        duration: Duration,
496        schedule_delay: Duration,
497    ) -> api::Result<()> {
498        let graph = self.check_team_valid(team).await?;
499        let peer = SyncPeer::new(peer, graph);
500        self.syncer
501            .sync_hello_subscribe(peer, graph_change_debounce, duration, schedule_delay)
502            .await?;
503        Ok(())
504    }
505
506    #[cfg(feature = "preview")]
507    #[instrument(skip(self), err)]
508    async fn sync_hello_unsubscribe(
509        self,
510        _: context::Context,
511        peer: Addr,
512        team: api::TeamId,
513    ) -> api::Result<()> {
514        let graph = self.check_team_valid(team).await?;
515        let peer = SyncPeer::new(peer, graph);
516        self.syncer.sync_hello_unsubscribe(peer).await?;
517        Ok(())
518    }
519
520    #[instrument(skip(self), err)]
521    async fn remove_sync_peer(
522        self,
523        _: context::Context,
524        peer: Addr,
525        team: api::TeamId,
526    ) -> api::Result<()> {
527        let graph = self.check_team_valid(team).await?;
528        let peer = SyncPeer::new(peer, graph);
529        self.syncer
530            .remove_peer(peer)
531            .await
532            .context("unable to remove sync peer")?;
533        Ok(())
534    }
535
536    //
537    // Local team management
538    //
539
540    #[instrument(skip(self))]
541    async fn add_team(mut self, _: context::Context, cfg: api::AddTeamConfig) -> api::Result<()> {
542        let team = cfg.team_id;
543        self.check_team_valid(team).await?;
544
545        match cfg.quic_sync {
546            Some(cfg) => self.add_team_quic_sync(team, cfg).await,
547            None => Err(anyhow!("Missing QUIC sync config").into()),
548        }
549    }
550
551    #[instrument(skip(self), err)]
552    async fn remove_team(self, _: context::Context, team: api::TeamId) -> api::Result<()> {
553        if let Some(data) = &self.quic {
554            self.remove_team_quic_sync(team, data)?;
555        }
556
557        self.seed_id_dir.remove(team).await?;
558
559        self.client
560            .lock_aranya()
561            .await
562            .remove_graph(GraphId::transmute(team))
563            .context("unable to remove graph from storage")?;
564
565        Ok(())
566    }
567
568    #[instrument(skip(self), err)]
569    async fn create_team(
570        mut self,
571        _: context::Context,
572        cfg: api::CreateTeamConfig,
573    ) -> api::Result<api::TeamId> {
574        info!("create_team");
575
576        let nonce = &mut [0u8; 16];
577        Rng.fill_bytes(nonce);
578        let pk = self.get_pk()?;
579        let (graph_id, _) = self
580            .client
581            .create_team(pk, Some(nonce))
582            .await
583            .context("unable to create team")?;
584        debug!(?graph_id);
585        let team_id = api::TeamId::transmute(graph_id);
586
587        match cfg.quic_sync {
588            Some(qs_cfg) => {
589                self.create_team_quic_sync(team_id, qs_cfg).await?;
590            }
591            None => {
592                warn!("Missing QUIC sync config");
593
594                let seed = qs::PskSeed::new(Rng, team_id);
595                self.add_seed(team_id, seed).await?;
596            }
597        }
598
599        Ok(team_id)
600    }
601
602    #[instrument(skip(self), err)]
603    async fn close_team(self, _: context::Context, team: api::TeamId) -> api::Result<()> {
604        let _graph = self.check_team_valid(team).await?;
605
606        todo!();
607    }
608
609    //
610    // Device onboarding
611    //
612
613    #[instrument(skip(self), err)]
614    async fn encrypt_psk_seed_for_peer(
615        self,
616        _: context::Context,
617        team: api::TeamId,
618        peer_enc_pk: EncryptionPublicKey<CS>,
619    ) -> aranya_daemon_api::Result<WrappedSeed> {
620        let enc_pk = self.pk.lock().expect("poisoned").enc_pk.clone();
621
622        let (seed, enc_sk) = {
623            let crypto = &mut *self.crypto.lock().await;
624            let seed = {
625                let seed_id = self.seed_id_dir.get(team).await?;
626                qs::PskSeed::load(&crypto.engine, &crypto.local_store, seed_id)?
627                    .context("no seed in dir")?
628            };
629            let enc_sk: EncryptionKey<CS> = crypto
630                .aranya_store
631                .get_key(&crypto.engine, enc_pk.id()?)
632                .context("keystore error")?
633                .context("missing enc_sk for encrypt seed")?;
634            (seed, enc_sk)
635        };
636
637        let group = GroupId::transmute(team);
638        let (encap_key, encrypted_seed) = enc_sk
639            .seal_psk_seed(Rng, &seed.0, &peer_enc_pk, &group)
640            .context("could not seal psk seed")?;
641
642        Ok(WrappedSeed {
643            sender_pk: enc_pk,
644            encap_key,
645            encrypted_seed,
646        })
647    }
648
649    #[instrument(skip(self), err)]
650    async fn add_device_to_team(
651        self,
652        _: context::Context,
653        team: api::TeamId,
654        keys: api::PublicKeyBundle,
655        initial_role: Option<api::RoleId>,
656    ) -> api::Result<()> {
657        let graph = self.check_team_valid(team).await?;
658
659        let effects = self
660            .client
661            .actions(graph)
662            .add_device(keys.into(), initial_role.map(RoleId::transmute))
663            .await
664            .context("unable to add device to team")?;
665        self.effect_handler.handle_effects(graph, &effects).await?;
666        Ok(())
667    }
668
669    #[instrument(skip(self), err)]
670    async fn remove_device_from_team(
671        self,
672        _: context::Context,
673        team: api::TeamId,
674        device: api::DeviceId,
675    ) -> api::Result<()> {
676        let graph = self.check_team_valid(team).await?;
677
678        let effects = self
679            .client
680            .actions(graph)
681            .remove_device(DeviceId::transmute(device))
682            .await
683            .context("unable to remove device from team")?;
684        self.effect_handler.handle_effects(graph, &effects).await?;
685
686        Ok(())
687    }
688
689    #[instrument(skip(self))]
690    async fn devices_on_team(
691        self,
692        _: context::Context,
693        team: api::TeamId,
694    ) -> api::Result<Box<[api::DeviceId]>> {
695        let graph = self.check_team_valid(team).await?;
696
697        let devices = self
698            .client
699            .actions(graph)
700            .query_devices_on_team()
701            .await
702            .context("unable to query devices on team")?
703            .into_iter()
704            .filter_map(|e| {
705                if let Effect::QueryDevicesOnTeamResult(e) = e {
706                    Some(api::DeviceId::from_base(e.device_id))
707                } else {
708                    warn!(name = e.name(), "unexpected effect");
709                    None
710                }
711            })
712            .collect();
713
714        Ok(devices)
715    }
716
717    #[instrument(skip(self), err)]
718    async fn device_public_key_bundle(
719        self,
720        _: context::Context,
721        team: api::TeamId,
722        device: api::DeviceId,
723    ) -> api::Result<api::PublicKeyBundle> {
724        let graph = self.check_team_valid(team).await?;
725
726        let effects = self
727            .client
728            .actions(graph)
729            .query_device_public_key_bundle(DeviceId::transmute(device))
730            .await
731            .context("unable to query device public key bundle")?;
732        if let Some(Effect::QueryDeviceKeyBundleResult(e)) =
733            find_effect!(effects, Effect::QueryDeviceKeyBundleResult(_e))
734        {
735            Ok(api::PublicKeyBundle::from(e.device_keys))
736        } else {
737            Err(anyhow!("unable to query device public key bundle").into())
738        }
739    }
740
741    #[instrument(skip(self), err)]
742    async fn labels_assigned_to_device(
743        self,
744        _: context::Context,
745        team: api::TeamId,
746        device: api::DeviceId,
747    ) -> api::Result<Box<[api::Label]>> {
748        let graph = self.check_team_valid(team).await?;
749
750        let effects = self
751            .client
752            .actions(graph)
753            .query_labels_assigned_to_device(DeviceId::transmute(device))
754            .await
755            .context("unable to query device label assignments")?;
756        let mut labels = Vec::new();
757        for e in effects {
758            if let Effect::QueryLabelsAssignedToDeviceResult(e) = e {
759                debug!("found label: {}", e.label_id);
760                labels.push(api::Label {
761                    id: api::LabelId::from_base(e.label_id),
762                    name: e.label_name,
763                    author_id: api::DeviceId::from_base(e.label_author_id),
764                });
765            }
766        }
767        return Ok(labels.into_boxed_slice());
768    }
769
770    #[instrument(skip(self), err)]
771    async fn device_role(
772        self,
773        _: context::Context,
774        team: api::TeamId,
775        device: api::DeviceId,
776    ) -> api::Result<Option<api::Role>> {
777        let graph = self.check_team_valid(team).await?;
778
779        let effects = self
780            .client
781            .actions(graph)
782            .query_device_role(DeviceId::transmute(device))
783            .await
784            .context("unable to query device role")?;
785        if let Some(Effect::QueryDeviceRoleResult(e)) =
786            find_effect!(&effects, Effect::QueryDeviceRoleResult(_))
787        {
788            Ok(Some(api::Role {
789                id: api::RoleId::from_base(e.role_id),
790                name: e.name.clone(),
791                author_id: api::DeviceId::from_base(e.author_id),
792                default: e.default,
793            }))
794        } else {
795            Ok(None)
796        }
797    }
798
799    #[cfg(feature = "preview")]
800    #[instrument(skip(self), err)]
801    async fn create_role(
802        self,
803        _: context::Context,
804        team: api::TeamId,
805        role_name: Text,
806        owning_role: api::RoleId,
807    ) -> api::Result<api::Role> {
808        let graph = self.check_team_valid(team).await?;
809
810        let effects = self
811            .client
812            .actions(graph)
813            .create_role(role_name, RoleId::transmute(owning_role))
814            .await
815            .context("unable to create role")?;
816        self.effect_handler.handle_effects(graph, &effects).await?;
817
818        if let Some(Effect::RoleCreated(e)) = find_effect!(&effects, Effect::RoleCreated(_)) {
819            Ok(api::Role {
820                id: api::RoleId::from_base(e.role_id),
821                name: e.name.clone(),
822                author_id: api::DeviceId::from_base(e.author_id),
823                default: e.default,
824            })
825        } else {
826            Err(anyhow!("wrong effect when creating role").into())
827        }
828    }
829
830    #[cfg(feature = "preview")]
831    #[instrument(skip(self), err)]
832    async fn delete_role(
833        self,
834        _: context::Context,
835        team: api::TeamId,
836        role_id: api::RoleId,
837    ) -> api::Result<()> {
838        let graph = self.check_team_valid(team).await?;
839
840        let effects = self
841            .client
842            .actions(graph)
843            .delete_role(RoleId::transmute(role_id))
844            .await
845            .context("unable to delete role")?;
846        self.effect_handler.handle_effects(graph, &effects).await?;
847
848        if let Some(Effect::RoleDeleted(e)) = find_effect!(&effects, Effect::RoleDeleted(_)) {
849            info!("Deleted role {role_id} ({})", e.name());
850            Ok(())
851        } else {
852            Err(anyhow!("wrong effect when creating role").into())
853        }
854    }
855
856    #[instrument(skip(self), err)]
857    async fn assign_role(
858        self,
859        _: context::Context,
860        team: api::TeamId,
861        device: api::DeviceId,
862        role: api::RoleId,
863    ) -> api::Result<()> {
864        let graph = self.check_team_valid(team).await?;
865
866        let effects = self
867            .client
868            .actions(graph)
869            .assign_role(DeviceId::transmute(device), RoleId::transmute(role))
870            .await
871            .context("unable to assign role")?;
872        self.effect_handler.handle_effects(graph, &effects).await?;
873
874        if let Some(Effect::RoleAssigned(_e)) = find_effect!(&effects, Effect::RoleAssigned(_e)) {
875            Ok(())
876        } else {
877            Err(anyhow!("unable to assign role").into())
878        }
879    }
880
881    #[instrument(skip(self), err)]
882    async fn revoke_role(
883        self,
884        _: context::Context,
885        team: api::TeamId,
886        device: api::DeviceId,
887        role: api::RoleId,
888    ) -> api::Result<()> {
889        let graph = self.check_team_valid(team).await?;
890
891        let effects = self
892            .client
893            .actions(graph)
894            .revoke_role(DeviceId::transmute(device), RoleId::transmute(role))
895            .await
896            .context("unable to revoke device role")?;
897        self.effect_handler.handle_effects(graph, &effects).await?;
898
899        if let Some(Effect::RoleRevoked(_e)) = find_effect!(&effects, Effect::RoleRevoked(_e)) {
900            Ok(())
901        } else {
902            Err(anyhow!("unable to revoke device role").into())
903        }
904    }
905
906    #[instrument(skip(self), err)]
907    async fn change_role(
908        self,
909        _: context::Context,
910        team: api::TeamId,
911        device_id: api::DeviceId,
912        old_role_id: api::RoleId,
913        new_role_id: api::RoleId,
914    ) -> api::Result<()> {
915        let graph = self.check_team_valid(team).await?;
916
917        let effects = self
918            .client
919            .actions(graph)
920            .change_role(
921                DeviceId::transmute(device_id),
922                RoleId::transmute(old_role_id),
923                RoleId::transmute(new_role_id),
924            )
925            .await
926            .context("unable to change device role")?;
927        self.effect_handler.handle_effects(graph, &effects).await?;
928
929        if let Some(Effect::RoleChanged(_e)) = find_effect!(&effects, Effect::RoleChanged(_e)) {
930            Ok(())
931        } else {
932            Err(anyhow!("unable to change device role").into())
933        }
934    }
935
936    #[cfg(feature = "afc")]
937    #[instrument(skip(self), err)]
938    async fn create_afc_channel(
939        self,
940        _: context::Context,
941        team: api::TeamId,
942        peer_id: api::DeviceId,
943        label: api::LabelId,
944    ) -> api::Result<api::AfcSendChannelInfo> {
945        let graph = self.check_team_valid(team).await?;
946
947        info!("creating afc uni channel");
948
949        let SessionData { ctrl, effects } = self
950            .client
951            .actions(graph)
952            .create_afc_uni_channel_off_graph(
953                DeviceId::transmute(peer_id),
954                LabelId::transmute(label),
955            )
956            .await?;
957
958        let [Effect::AfcUniChannelCreated(e)] = effects.as_slice() else {
959            bug!("expected afc uni channel created effect")
960        };
961
962        self.effect_handler.handle_effects(graph, &effects).await?;
963
964        let (local_channel_id, channel_id) = self.afc.uni_channel_created(e).await?;
965        info!("afc uni channel created");
966
967        let ctrl = get_afc_ctrl(ctrl)?;
968
969        Ok(api::AfcSendChannelInfo {
970            ctrl,
971            local_channel_id,
972            channel_id,
973        })
974    }
975
976    #[cfg(feature = "afc")]
977    #[instrument(skip(self), err)]
978    async fn delete_afc_channel(
979        self,
980        _: context::Context,
981        chan: api::AfcLocalChannelId,
982    ) -> api::Result<()> {
983        self.afc.delete_channel(chan).await?;
984        info!("afc channel deleted");
985        Ok(())
986    }
987
988    #[cfg(feature = "afc")]
989    #[instrument(skip(self), err)]
990    async fn accept_afc_channel(
991        self,
992        _: context::Context,
993        team: api::TeamId,
994        ctrl: api::AfcCtrl,
995    ) -> api::Result<api::AfcReceiveChannelInfo> {
996        let graph = self.check_team_valid(team).await?;
997
998        let mut session = self.client.session_new(graph).await?;
999
1000        let effects = self.client.session_receive(&mut session, &ctrl).await?;
1001
1002        let [Effect::AfcUniChannelReceived(e)] = effects.as_slice() else {
1003            bug!("expected afc uni channel received effect")
1004        };
1005
1006        self.effect_handler.handle_effects(graph, &effects).await?;
1007
1008        let (local_channel_id, channel_id) = self.afc.uni_channel_received(e).await?;
1009
1010        return Ok(api::AfcReceiveChannelInfo {
1011            local_channel_id,
1012            channel_id,
1013            label_id: api::LabelId::from_base(e.label_id),
1014            peer_id: api::DeviceId::from_base(e.sender_id),
1015        });
1016    }
1017
1018    #[instrument(skip(self), err)]
1019    async fn create_label(
1020        self,
1021        _: context::Context,
1022        team: api::TeamId,
1023        label_name: Text,
1024        managing_role_id: api::RoleId,
1025    ) -> api::Result<api::LabelId> {
1026        let graph = self.check_team_valid(team).await?;
1027
1028        let effects = self
1029            .client
1030            .actions(graph)
1031            .create_label(label_name, RoleId::transmute(managing_role_id))
1032            .await
1033            .context("unable to create label")?;
1034        self.effect_handler.handle_effects(graph, &effects).await?;
1035
1036        if let Some(Effect::LabelCreated(e)) = find_effect!(&effects, Effect::LabelCreated(_e)) {
1037            Ok(api::LabelId::from_base(e.label_id))
1038        } else {
1039            Err(anyhow!("unable to create label").into())
1040        }
1041    }
1042
1043    #[instrument(skip(self), err)]
1044    async fn delete_label(
1045        self,
1046        _: context::Context,
1047        team: api::TeamId,
1048        label_id: api::LabelId,
1049    ) -> api::Result<()> {
1050        let graph = self.check_team_valid(team).await?;
1051
1052        let effects = self
1053            .client
1054            .actions(graph)
1055            .delete_label(LabelId::transmute(label_id))
1056            .await
1057            .context("unable to delete label")?;
1058        self.effect_handler.handle_effects(graph, &effects).await?;
1059
1060        if let Some(Effect::LabelDeleted(_e)) = find_effect!(&effects, Effect::LabelDeleted(_e)) {
1061            Ok(())
1062        } else {
1063            Err(anyhow!("unable to delete label").into())
1064        }
1065    }
1066
1067    async fn add_label_managing_role(
1068        self,
1069        _: context::Context,
1070        team: api::TeamId,
1071        label_id: api::LabelId,
1072        managing_role_id: api::RoleId,
1073    ) -> api::Result<()> {
1074        let graph = self.check_team_valid(team).await?;
1075
1076        let effects = self
1077            .client
1078            .actions(graph)
1079            .add_label_managing_role(
1080                LabelId::transmute(label_id),
1081                RoleId::transmute(managing_role_id),
1082            )
1083            .await
1084            .context("unable to add label managing role")?;
1085        self.effect_handler.handle_effects(graph, &effects).await?;
1086
1087        if let Some(Effect::LabelManagingRoleAdded(_e)) =
1088            find_effect!(&effects, Effect::LabelManagingRoleAdded(_e))
1089        {
1090            Ok(())
1091        } else {
1092            Err(anyhow!("unable to add label managing role").into())
1093        }
1094    }
1095
1096    #[instrument(skip(self), err)]
1097    async fn assign_label_to_device(
1098        self,
1099        _: context::Context,
1100        team: api::TeamId,
1101        device: api::DeviceId,
1102        label_id: api::LabelId,
1103        op: api::ChanOp,
1104    ) -> api::Result<()> {
1105        let graph = self.check_team_valid(team).await?;
1106
1107        let effects = self
1108            .client
1109            .actions(graph)
1110            .assign_label_to_device(
1111                DeviceId::transmute(device),
1112                LabelId::transmute(label_id),
1113                op.into(),
1114            )
1115            .await
1116            .context("unable to assign label")?;
1117        self.effect_handler.handle_effects(graph, &effects).await?;
1118
1119        if let Some(Effect::AssignedLabelToDevice(_e)) =
1120            find_effect!(&effects, Effect::AssignedLabelToDevice(_e))
1121        {
1122            Ok(())
1123        } else {
1124            Err(anyhow!("unable to assign label").into())
1125        }
1126    }
1127
1128    #[instrument(skip(self), err)]
1129    async fn revoke_label_from_device(
1130        self,
1131        _: context::Context,
1132        team: api::TeamId,
1133        device: api::DeviceId,
1134        label_id: api::LabelId,
1135    ) -> api::Result<()> {
1136        let graph = self.check_team_valid(team).await?;
1137
1138        let effects = self
1139            .client
1140            .actions(graph)
1141            .revoke_label_from_device(DeviceId::transmute(device), LabelId::transmute(label_id))
1142            .await
1143            .context("unable to revoke label")?;
1144        self.effect_handler.handle_effects(graph, &effects).await?;
1145
1146        if let Some(Effect::LabelRevokedFromDevice(_e)) =
1147            find_effect!(&effects, Effect::LabelRevokedFromDevice(_e))
1148        {
1149            Ok(())
1150        } else {
1151            Err(anyhow!("unable to revoke label").into())
1152        }
1153    }
1154
1155    #[instrument(skip(self), err)]
1156    async fn label(
1157        self,
1158        _: context::Context,
1159        team: api::TeamId,
1160        label_id: api::LabelId,
1161    ) -> api::Result<Option<api::Label>> {
1162        let graph = self.check_team_valid(team).await?;
1163
1164        let effects = self
1165            .client
1166            .actions(graph)
1167            .query_label(LabelId::transmute(label_id))
1168            .await
1169            .context("unable to query label")?;
1170        if let Some(Effect::QueryLabelResult(e)) =
1171            find_effect!(&effects, Effect::QueryLabelResult(_e))
1172        {
1173            Ok(Some(api::Label {
1174                id: api::LabelId::from_base(e.label_id),
1175                name: e.label_name.clone(),
1176                author_id: api::DeviceId::from_base(e.label_author_id),
1177            }))
1178        } else {
1179            Ok(None)
1180        }
1181    }
1182
1183    #[instrument(skip(self), err)]
1184    async fn labels(self, _: context::Context, team: api::TeamId) -> api::Result<Vec<api::Label>> {
1185        let graph = self.check_team_valid(team).await?;
1186
1187        let effects = self
1188            .client
1189            .actions(graph)
1190            .query_labels()
1191            .await
1192            .context("unable to query labels")?;
1193        let mut labels: Vec<api::Label> = Vec::new();
1194        for e in effects {
1195            if let Effect::QueryLabelsResult(e) = e {
1196                debug!("found label: {}", e.label_id);
1197                labels.push(api::Label {
1198                    id: api::LabelId::from_base(e.label_id),
1199                    name: e.label_name.clone(),
1200                    author_id: api::DeviceId::from_base(e.label_author_id),
1201                });
1202            }
1203        }
1204        Ok(labels)
1205    }
1206
1207    #[instrument(skip(self), err)]
1208    async fn setup_default_roles(
1209        self,
1210        _: context::Context,
1211        team: api::TeamId,
1212        owning_role: api::RoleId,
1213    ) -> api::Result<Box<[api::Role]>> {
1214        let graph = self.check_team_valid(team).await?;
1215
1216        let effects = self
1217            .client
1218            .actions(graph)
1219            .setup_default_roles(RoleId::transmute(owning_role))
1220            .await
1221            .context("unable to setup default roles")?;
1222        self.effect_handler.handle_effects(graph, &effects).await?;
1223
1224        let roles = effects
1225            .into_iter()
1226            .filter_map(|e| {
1227                if let Effect::RoleCreated(e @ RoleCreated { default: true, .. }) = e {
1228                    Some(api::Role {
1229                        id: api::RoleId::from_base(e.role_id),
1230                        name: e.name,
1231                        author_id: api::DeviceId::from_base(e.author_id),
1232                        default: e.default,
1233                    })
1234                } else {
1235                    warn!(name = e.name(), "unexpected effect");
1236                    None
1237                }
1238            })
1239            .collect();
1240
1241        Ok(roles)
1242    }
1243
1244    #[instrument(skip(self), err)]
1245    async fn team_roles(
1246        self,
1247        _: context::Context,
1248        team: api::TeamId,
1249    ) -> api::Result<Box<[api::Role]>> {
1250        let graph = self.check_team_valid(team).await?;
1251
1252        let roles = self
1253            .client
1254            .actions(graph)
1255            .query_team_roles()
1256            .await
1257            .context("unable to query team roles")?
1258            .into_iter()
1259            .filter_map(|e| {
1260                if let Effect::QueryTeamRolesResult(e) = e {
1261                    Some(api::Role {
1262                        id: api::RoleId::from_base(e.role_id),
1263                        name: e.name,
1264                        author_id: api::DeviceId::from_base(e.author_id),
1265                        default: e.default,
1266                    })
1267                } else {
1268                    warn!(name = e.name(), "unexpected effect");
1269                    None
1270                }
1271            })
1272            .collect();
1273        Ok(roles)
1274    }
1275
1276    //
1277    // Role management
1278    //
1279
1280    #[cfg(feature = "preview")]
1281    #[instrument(skip(self), err)]
1282    async fn add_perm_to_role(
1283        self,
1284        context: context::Context,
1285        team: api::TeamId,
1286        role: api::RoleId,
1287        perm: api::SimplePerm,
1288    ) -> api::Result<()> {
1289        let graph = self.check_team_valid(team).await?;
1290
1291        let effects = self
1292            .client
1293            .actions(graph)
1294            .add_perm_to_role(RoleId::transmute(role), perm.into())
1295            .await
1296            .context("unable to add permission to role")?;
1297        self.effect_handler.handle_effects(graph, &effects).await?;
1298
1299        Ok(())
1300    }
1301
1302    #[cfg(feature = "preview")]
1303    #[instrument(skip(self), err)]
1304    async fn remove_perm_from_role(
1305        self,
1306        context: context::Context,
1307        team: api::TeamId,
1308        role: api::RoleId,
1309        perm: api::SimplePerm,
1310    ) -> api::Result<()> {
1311        let graph = self.check_team_valid(team).await?;
1312
1313        let effects = self
1314            .client
1315            .actions(graph)
1316            .remove_perm_from_role(RoleId::transmute(role), perm.into())
1317            .await
1318            .context("unable to add permission to role")?;
1319        self.effect_handler.handle_effects(graph, &effects).await?;
1320
1321        Ok(())
1322    }
1323
1324    #[cfg(feature = "preview")]
1325    #[instrument(skip(self), err)]
1326    async fn add_role_owner(
1327        self,
1328        _: context::Context,
1329        team: api::TeamId,
1330        role: api::RoleId,
1331        owning_role: api::RoleId,
1332    ) -> api::Result<()> {
1333        let graph = self.check_team_valid(team).await?;
1334
1335        let effects = self
1336            .client
1337            .actions(graph)
1338            .add_role_owner(RoleId::transmute(role), RoleId::transmute(owning_role))
1339            .await
1340            .context("unable to add role owner")?;
1341        self.effect_handler.handle_effects(graph, &effects).await?;
1342
1343        Ok(())
1344    }
1345
1346    #[cfg(feature = "preview")]
1347    #[instrument(skip(self), err)]
1348    async fn remove_role_owner(
1349        self,
1350        _: context::Context,
1351        team: api::TeamId,
1352        role: api::RoleId,
1353        owning_role: api::RoleId,
1354    ) -> api::Result<()> {
1355        let graph = self.check_team_valid(team).await?;
1356
1357        let effects = self
1358            .client
1359            .actions(graph)
1360            .remove_role_owner(RoleId::transmute(role), RoleId::transmute(owning_role))
1361            .await
1362            .context("unable to remove role owner")?;
1363        self.effect_handler.handle_effects(graph, &effects).await?;
1364
1365        Ok(())
1366    }
1367
1368    #[instrument(skip(self), err)]
1369    async fn role_owners(
1370        self,
1371        _: context::Context,
1372        team: api::TeamId,
1373        role: api::RoleId,
1374    ) -> api::Result<Box<[api::Role]>> {
1375        let graph = self.check_team_valid(team).await?;
1376
1377        let roles = self
1378            .client
1379            .actions(graph)
1380            .query_role_owners(RoleId::transmute(role))
1381            .await
1382            .context("unable to query role owners")?
1383            .into_iter()
1384            .filter_map(|e| {
1385                if let Effect::QueryRoleOwnersResult(e) = e {
1386                    Some(api::Role {
1387                        id: api::RoleId::from_base(e.role_id),
1388                        name: e.name,
1389                        author_id: api::DeviceId::from_base(e.author_id),
1390                        default: e.default,
1391                    })
1392                } else {
1393                    None
1394                }
1395            })
1396            .collect();
1397        Ok(roles)
1398    }
1399
1400    #[cfg(feature = "preview")]
1401    #[instrument(skip(self), err)]
1402    async fn assign_role_management_perm(
1403        self,
1404        _: context::Context,
1405        team: api::TeamId,
1406        role: api::RoleId,
1407        managing_role: api::RoleId,
1408        perm: api::RoleManagementPerm,
1409    ) -> api::Result<()> {
1410        let graph = self.check_team_valid(team).await?;
1411
1412        let effects = self
1413            .client
1414            .actions(graph)
1415            .assign_role_management_perm(
1416                RoleId::transmute(role),
1417                RoleId::transmute(managing_role),
1418                perm.into(),
1419            )
1420            .await
1421            .context("unable to assign role management permission")?;
1422        self.effect_handler.handle_effects(graph, &effects).await?;
1423
1424        Ok(())
1425    }
1426
1427    #[cfg(feature = "preview")]
1428    #[instrument(skip(self), err)]
1429    async fn revoke_role_management_perm(
1430        self,
1431        _: context::Context,
1432        team: api::TeamId,
1433        role: api::RoleId,
1434        managing_role: api::RoleId,
1435        perm: api::RoleManagementPerm,
1436    ) -> api::Result<()> {
1437        let graph = self.check_team_valid(team).await?;
1438
1439        let effects = self
1440            .client
1441            .actions(graph)
1442            .revoke_role_management_perm(
1443                RoleId::transmute(role),
1444                RoleId::transmute(managing_role),
1445                perm.into(),
1446            )
1447            .await
1448            .context("unable to revoke role management permission")?;
1449        self.effect_handler.handle_effects(graph, &effects).await?;
1450
1451        Ok(())
1452    }
1453}
1454
1455impl Api {
1456    async fn add_seed(&mut self, team: api::TeamId, seed: qs::PskSeed) -> anyhow::Result<()> {
1457        let crypto = &mut *self.crypto.lock().await;
1458
1459        let id = crypto
1460            .local_store
1461            .insert_key(&crypto.engine, seed.into_inner())
1462            .context("inserting seed")?;
1463
1464        if let Err(e) = self
1465            .seed_id_dir
1466            .append(team, id)
1467            .await
1468            .context("could not write seed id to file")
1469        {
1470            match crypto
1471                .local_store
1472                .remove::<WrappedKey<CS>>(id.as_base())
1473                .context("could not remove seed from keystore")
1474            {
1475                Ok(_) => return Err(e),
1476                Err(inner) => return Err(e).context(inner),
1477            }
1478        };
1479
1480        Ok(())
1481    }
1482}
1483
1484impl From<api::PublicKeyBundle> for PublicKeyBundle {
1485    fn from(value: api::PublicKeyBundle) -> Self {
1486        PublicKeyBundle {
1487            ident_key: value.identity,
1488            sign_key: value.signing,
1489            enc_key: value.encryption,
1490        }
1491    }
1492}
1493
1494impl From<PublicKeyBundle> for api::PublicKeyBundle {
1495    fn from(value: PublicKeyBundle) -> Self {
1496        api::PublicKeyBundle {
1497            identity: value.ident_key,
1498            signing: value.sign_key,
1499            encryption: value.enc_key,
1500        }
1501    }
1502}
1503
1504impl From<api::ChanOp> for ChanOp {
1505    fn from(value: api::ChanOp) -> Self {
1506        match value {
1507            api::ChanOp::SendRecv => ChanOp::SendRecv,
1508            api::ChanOp::RecvOnly => ChanOp::RecvOnly,
1509            api::ChanOp::SendOnly => ChanOp::SendOnly,
1510        }
1511    }
1512}
1513
1514impl From<ChanOp> for api::ChanOp {
1515    fn from(value: ChanOp) -> Self {
1516        match value {
1517            ChanOp::SendRecv => api::ChanOp::SendRecv,
1518            ChanOp::RecvOnly => api::ChanOp::RecvOnly,
1519            ChanOp::SendOnly => api::ChanOp::SendOnly,
1520        }
1521    }
1522}
1523
1524impl From<api::RoleManagementPerm> for RoleManagementPerm {
1525    fn from(value: api::RoleManagementPerm) -> Self {
1526        match value {
1527            api::RoleManagementPerm::CanAssignRole => RoleManagementPerm::CanAssignRole,
1528            api::RoleManagementPerm::CanRevokeRole => RoleManagementPerm::CanRevokeRole,
1529            api::RoleManagementPerm::CanChangeRolePerms => RoleManagementPerm::CanChangeRolePerms,
1530        }
1531    }
1532}
1533
1534impl From<RoleManagementPerm> for api::RoleManagementPerm {
1535    fn from(value: RoleManagementPerm) -> Self {
1536        match value {
1537            RoleManagementPerm::CanAssignRole => api::RoleManagementPerm::CanAssignRole,
1538            RoleManagementPerm::CanRevokeRole => api::RoleManagementPerm::CanRevokeRole,
1539            RoleManagementPerm::CanChangeRolePerms => api::RoleManagementPerm::CanChangeRolePerms,
1540        }
1541    }
1542}
1543
1544impl From<api::SimplePerm> for SimplePerm {
1545    fn from(value: api::SimplePerm) -> Self {
1546        match value {
1547            api::SimplePerm::AddDevice => SimplePerm::AddDevice,
1548            api::SimplePerm::RemoveDevice => SimplePerm::RemoveDevice,
1549            api::SimplePerm::TerminateTeam => SimplePerm::TerminateTeam,
1550            api::SimplePerm::CreateRole => SimplePerm::CreateRole,
1551            api::SimplePerm::DeleteRole => SimplePerm::DeleteRole,
1552            api::SimplePerm::AssignRole => SimplePerm::AssignRole,
1553            api::SimplePerm::RevokeRole => SimplePerm::RevokeRole,
1554            api::SimplePerm::ChangeRoleManagementPerms => SimplePerm::ChangeRoleManagementPerms,
1555            api::SimplePerm::SetupDefaultRole => SimplePerm::SetupDefaultRole,
1556            api::SimplePerm::ChangeRoleManagingRole => SimplePerm::ChangeRoleManagingRole,
1557            api::SimplePerm::CreateLabel => SimplePerm::CreateLabel,
1558            api::SimplePerm::DeleteLabel => SimplePerm::DeleteLabel,
1559            api::SimplePerm::ChangeLabelManagingRole => SimplePerm::ChangeLabelManagingRole,
1560            api::SimplePerm::AssignLabel => SimplePerm::AssignLabel,
1561            api::SimplePerm::RevokeLabel => SimplePerm::RevokeLabel,
1562            api::SimplePerm::CanUseAfc => SimplePerm::CanUseAfc,
1563            api::SimplePerm::CreateAfcUniChannel => SimplePerm::CreateAfcUniChannel,
1564        }
1565    }
1566}
1567
1568impl From<SimplePerm> for api::SimplePerm {
1569    fn from(value: SimplePerm) -> Self {
1570        match value {
1571            SimplePerm::AddDevice => api::SimplePerm::AddDevice,
1572            SimplePerm::RemoveDevice => api::SimplePerm::RemoveDevice,
1573            SimplePerm::TerminateTeam => api::SimplePerm::TerminateTeam,
1574            SimplePerm::CreateRole => api::SimplePerm::CreateRole,
1575            SimplePerm::DeleteRole => api::SimplePerm::DeleteRole,
1576            SimplePerm::AssignRole => api::SimplePerm::AssignRole,
1577            SimplePerm::RevokeRole => api::SimplePerm::RevokeRole,
1578            SimplePerm::ChangeRoleManagementPerms => api::SimplePerm::ChangeRoleManagementPerms,
1579            SimplePerm::SetupDefaultRole => api::SimplePerm::SetupDefaultRole,
1580            SimplePerm::ChangeRoleManagingRole => api::SimplePerm::ChangeRoleManagingRole,
1581            SimplePerm::CreateLabel => api::SimplePerm::CreateLabel,
1582            SimplePerm::DeleteLabel => api::SimplePerm::DeleteLabel,
1583            SimplePerm::ChangeLabelManagingRole => api::SimplePerm::ChangeLabelManagingRole,
1584            SimplePerm::AssignLabel => api::SimplePerm::AssignLabel,
1585            SimplePerm::RevokeLabel => api::SimplePerm::RevokeLabel,
1586            SimplePerm::CanUseAfc => api::SimplePerm::CanUseAfc,
1587            SimplePerm::CreateAfcUniChannel => api::SimplePerm::CreateAfcUniChannel,
1588        }
1589    }
1590}
1591
1592/// Extract a single command from the session commands to get the AFC control message.
1593#[cfg(feature = "afc")]
1594fn get_afc_ctrl(cmds: Vec<Box<[u8]>>) -> anyhow::Result<Box<[u8]>> {
1595    let mut cmds = cmds.into_iter();
1596    let msg = cmds.next().context("missing AFC control message")?;
1597    if cmds.next().is_some() {
1598        anyhow::bail!("too many commands for AFC control message");
1599    }
1600    Ok(msg)
1601}