aranya-client 6.0.0

Client library for using Aranya
Documentation
use std::slice;

use aranya_daemon_api as api;
use aranya_id::custom_id;
use serde::{Deserialize, Serialize};
use tracing::instrument;

use crate::{
    client::{ChanOp, Client, Label, LabelId, Labels, Role, RoleId},
    error::{aranya_error, IpcError, Result},
    util::{impl_slice_iter_wrapper, rpc_context, ApiConv as _, ApiId},
};

/// A device's public key bundle.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(transparent)]
pub struct PublicKeyBundle(api::PublicKeyBundle);

/// See [`PublicKeyBundle`].
#[deprecated(note = "use `PublicKeyBundle`")]
pub type KeyBundle = PublicKeyBundle;

impl PublicKeyBundle {
    pub(crate) fn from_api(api: api::PublicKeyBundle) -> Self {
        Self(api)
    }

    pub(crate) fn into_api(self) -> api::PublicKeyBundle {
        self.0
    }

    /// Return public encryption key bytes.
    pub fn encryption(&self) -> &[u8] {
        &self.0.encryption
    }
}

custom_id! {
    /// Uniquely identifies a device.
    pub struct DeviceId;
}
impl ApiId<api::DeviceId> for DeviceId {}

/// A list of [`DeviceId`].
#[derive(Debug)]
pub struct Devices {
    pub(super) data: Box<[DeviceId]>,
}

impl Devices {
    /// Returns an iterator over the [`DeviceId`]s.
    pub fn iter(&self) -> IterDevices<'_> {
        IterDevices(self.data.iter())
    }

    #[doc(hidden)]
    pub fn __data(&self) -> &[DeviceId] {
        &self.data
    }
}

/// An iterator over [`DeviceId`]s.
#[derive(Clone, Debug)]
pub struct IterDevices<'a>(slice::Iter<'a, DeviceId>);

impl_slice_iter_wrapper!(IterDevices<'a> for DeviceId);

/// Represents an Aranya device
#[derive(Debug)]
pub struct Device<'a> {
    pub(super) client: &'a Client,
    pub(super) id: api::DeviceId,
    pub(super) team_id: api::TeamId,
}

impl Device<'_> {
    /// Returns the device's globally unique ID.
    pub fn id(&self) -> DeviceId {
        DeviceId::from_api(self.id)
    }

    /// See [`Self::public_key_bundle`].
    #[deprecated(note = "Use `public_key_bundle`.")]
    pub async fn keybundle(&self) -> Result<PublicKeyBundle> {
        self.public_key_bundle().await
    }

    /// Returns device's public key bundle.
    pub async fn public_key_bundle(&self) -> Result<PublicKeyBundle> {
        self.client
            .daemon
            .device_public_key_bundle(rpc_context(), self.team_id, self.id)
            .await
            .map_err(IpcError::new)?
            .map_err(aranya_error)
            .map(PublicKeyBundle::from_api)
    }

    /// Removes `device` from the team.
    ///
    /// A device can always remove itself. Removing another device requires:
    /// - `RemoveDevice` permission
    /// - `caller_rank > device_rank`
    #[instrument(skip(self))]
    pub async fn remove_from_team(&self) -> Result<()> {
        self.client
            .daemon
            .remove_device_from_team(rpc_context(), self.team_id, self.id)
            .await
            .map_err(IpcError::new)?
            .map_err(aranya_error)
    }

    /// Assigns `role` to `device`.
    ///
    /// Requires:
    /// - `AssignRole` permission
    /// - `caller_rank > device_rank` and `caller_rank > role_rank`
    #[instrument(skip(self))]
    pub async fn assign_role(&self, role: RoleId) -> Result<()> {
        self.client
            .daemon
            .assign_role(rpc_context(), self.team_id, self.id, role.into_api())
            .await
            .map_err(IpcError::new)?
            .map_err(aranya_error)
    }

    /// Revokes `role` from `device`.
    ///
    /// Requires:
    /// - `RevokeRole` permission
    /// - `caller_rank > device_rank` and `caller_rank > role_rank`
    #[instrument(skip(self))]
    pub async fn revoke_role(&self, role: RoleId) -> Result<()> {
        self.client
            .daemon
            .revoke_role(rpc_context(), self.team_id, self.id, role.into_api())
            .await
            .map_err(IpcError::new)?
            .map_err(aranya_error)
    }

    /// Changes the `role` on a `device`.
    ///
    /// Requires:
    /// - `RevokeRole` permission (for `old_role`)
    /// - `AssignRole` permission (for `new_role`)
    /// - `caller_rank > device_rank`, `caller_rank > old_role_rank`,
    ///   and `caller_rank > new_role_rank`
    #[instrument(skip(self))]
    pub async fn change_role(&self, old_role: RoleId, new_role: RoleId) -> Result<()> {
        self.client
            .daemon
            .change_role(
                rpc_context(),
                self.team_id,
                self.id,
                old_role.into_api(),
                new_role.into_api(),
            )
            .await
            .map_err(IpcError::new)?
            .map_err(aranya_error)
    }

    /// Returns the role assigned to the device, if any.
    pub async fn role(&self) -> Result<Option<Role>> {
        let role = self
            .client
            .daemon
            .device_role(rpc_context(), self.team_id, self.id)
            .await
            .map_err(IpcError::new)?
            .map_err(aranya_error)?
            .map(Role::from_api);
        Ok(role)
    }

    /// Returns a list of labels assiged to the device.
    pub async fn label_assignments(&self) -> Result<Labels> {
        let data = self
            .client
            .daemon
            .labels_assigned_to_device(rpc_context(), self.team_id, self.id)
            .await
            .map_err(IpcError::new)?
            .map_err(aranya_error)?
            // This _should_ just be `into_iter`, but the
            // compiler chooses the `&Box` impl. It's the same
            // end result, though.
            .into_vec()
            .into_iter()
            .map(Label::from_api)
            .collect();
        Ok(Labels { labels: data })
    }

    /// Assigns `label` to the device.
    ///
    /// Requires:
    /// - `AssignLabel` permission
    /// - `caller_rank > device_rank` and `caller_rank > label_rank`
    #[instrument(skip(self))]
    pub async fn assign_label(&self, label: LabelId, op: ChanOp) -> Result<()> {
        self.client
            .daemon
            .assign_label_to_device(rpc_context(), self.team_id, self.id, label.into_api(), op)
            .await
            .map_err(IpcError::new)?
            .map_err(aranya_error)
    }

    /// Revokes `label` from the device.
    ///
    /// Requires:
    /// - `RevokeLabel` permission
    /// - `caller_rank > device_rank` and `caller_rank > label_rank`
    #[instrument(skip(self))]
    pub async fn revoke_label(&self, label: LabelId) -> Result<()> {
        self.client
            .daemon
            .revoke_label_from_device(rpc_context(), self.team_id, self.id, label.into_api())
            .await
            .map_err(IpcError::new)?
            .map_err(aranya_error)
    }

    /// Returns the generation counter for the device, if any.
    #[cfg(test)]
    pub async fn generation(&self) -> Result<Option<i64>> {
        let gen = self
            .client
            .daemon
            .query_device_generation(rpc_context(), self.team_id, self.id)
            .await
            .map_err(IpcError::new)?
            .map_err(aranya_error)?;
        Ok(gen)
    }
}