ironoxide 0.26.0

A pure-Rust SDK for accessing IronCore's privacy platform
Documentation
//! Group API
//!
//! See [GroupOps](trait.GroupOps.html) for group functions and key terms.

pub use crate::internal::group_api::{
    GroupAccessEditErr, GroupAccessEditResult, GroupCreateResult, GroupGetResult, GroupId,
    GroupListResult, GroupMetaResult, GroupName, GroupUpdatePrivateKeyResult,
};
use crate::{
    common::SdkOperation,
    internal::{add_optional_timeout, group_api, group_api::GroupCreateOptsStd},
    user::UserId,
    IronOxideErr, Result,
};
use async_trait::async_trait;
use vec1::Vec1;

/// Options for group creation.
///
/// Default values are provided with [GroupCreateOpts::default()](struct.GroupCreateOpts.html#method.default)
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct GroupCreateOpts {
    /// ID of the group. If `None`, the server will assign the ID.
    id: Option<GroupId>,
    /// Name of the group.
    name: Option<GroupName>,
    /// - `true` (default) - creating user will be added as an admin of the group.
    /// - `false` - creating user will not be added as an admin of the group.
    add_as_admin: bool,
    /// - `true` (default) - creating user will be added to the group's membership.
    /// - `false` - creating user will not be added to the group's membership
    add_as_member: bool,
    /// Specifies who the owner of this group is. Group owners have the same permissions as other admins but they cannot be removed as an administrator.
    /// - `None` (default) - The creating user will be the owner of the group. Cannot be used if `add_as_admin` is set to false as the owner must be an admin.
    /// - `Some` - The provided user will be the owner of the group. This ID will automatically be added to the admins list.
    owner: Option<UserId>,
    /// List of users to add as admins of the group. Even if `add_as_admin` is false, the calling user will be added as an admin if they are in this list.
    admins: Vec<UserId>,
    /// List of users to add as members of the group. Even if `add_as_member` is false, the calling user will be added as a member if they are in this list.
    members: Vec<UserId>,
    /// - `true` - group's private key will be marked for rotation
    /// - `false` (default) - group's private key will not be marked for rotation
    needs_rotation: bool,
}

impl GroupCreateOpts {
    /// # Arguments
    /// - `id`
    ///     - `None` (default) - The server will assign the group's ID.
    ///     - `Some` - The provided ID will be used as the group's ID.
    /// - `name`
    ///     - `None` (default) - The group will be created with no name.
    ///     - `Some` - The provided name will be used as the group's name.
    /// - `add_as_admin`
    ///     - `true` (default) - The creating user will be added as a group admin.
    ///     - `false` - The creating user will not be a group admin.
    /// - `add_as_member`
    ///     - `true` (default) - The creating user will be added as a group member.
    ///     - `false` - The creating user will not be a group member.
    /// - `owner`
    ///     - `None` (default) - The creating user will be the owner of the group.
    ///     - `Some` - The provided user will be the owner of the group. This ID will automatically be added to the admin list.
    /// - `admins`
    ///     - The list of users to be added as group admins. This list takes priority over `add_as_admin`,
    ///       so the calling user will be added as an admin if they are in this list even if `add_as_admin` is false.
    /// - `members`
    ///     - The list of users to be added as members of the group. This list takes priority over `add_as_member`,
    ///       so the calling user will be added as a member if they are in this list even if `add_as_member` is false.
    /// - `needs_rotation`
    ///     - `true` - The group's private key will be marked for rotation.
    ///     - `false` (default) - The group's private key will not be marked for rotation.
    pub fn new(
        id: Option<GroupId>,
        name: Option<GroupName>,
        add_as_admin: bool,
        add_as_member: bool,
        owner: Option<UserId>,
        admins: Vec<UserId>,
        members: Vec<UserId>,
        needs_rotation: bool,
    ) -> GroupCreateOpts {
        GroupCreateOpts {
            id,
            name,
            add_as_admin,
            add_as_member,
            owner,
            admins,
            members,
            needs_rotation,
        }
    }

    fn standardize(self, calling_id: &UserId) -> Result<GroupCreateOptsStd> {
        // if `add_as_member`, make sure the calling user is in the `members` list
        let standardized_members = if self.add_as_member && !self.members.contains(calling_id) {
            let mut members = self.members.clone();
            members.push(calling_id.clone());
            members
        } else {
            self.members
        };
        let (standardized_admins, owner_id) = {
            // if `add_as_admin`, make sure the calling user is in the `admins` list
            let mut admins = if self.add_as_admin && !self.admins.contains(calling_id) {
                let mut admins = self.admins.clone();
                admins.push(calling_id.clone());
                admins
            } else {
                self.admins
            };
            let owner: &UserId = match &self.owner {
                Some(owner_id) => {
                    // if the owner is specified, make sure they're in the `admins` list
                    if !admins.contains(owner_id) {
                        admins.push(owner_id.clone());
                    }
                    owner_id
                }
                // if the owner is the calling user (default), they should have been added to the
                // admins list by `add_as_admin`. If they aren't, it will error later on.
                None => calling_id,
            };
            (admins, owner)
        };

        let non_empty_admins = Vec1::try_from_vec(standardized_admins).map_err(|_| {
            IronOxideErr::ValidationError(
                "admins".to_string(),
                "admins list cannot be empty".to_string(),
            )
        })?;

        if !non_empty_admins.contains(owner_id) {
            Err(IronOxideErr::ValidationError(
                "admins".to_string(),
                "admins list must contain the owner".to_string(),
            ))
        } else {
            Ok(GroupCreateOptsStd {
                id: self.id,
                name: self.name,
                owner: self.owner,
                admins: non_empty_admins,
                members: standardized_members,
                needs_rotation: self.needs_rotation,
            })
        }
    }
}

impl Default for GroupCreateOpts {
    /// Default `GroupCreateOpts` for common use cases.
    ///
    /// The group will be assigned an ID and have an empty name. The user who calls [group_create](trait.GroupOps.html#tymethod.group_create)
    /// will be the owner of the group as well as the only admin and member of the group. The group's private key will not be marked for rotation.
    fn default() -> Self {
        GroupCreateOpts::new(None, None, true, true, None, vec![], vec![], false)
    }
}

/// IronOxide Group Operations
///
/// # Key Terms
/// - ID     - The ID representing a group. It must be unique within the group's segment and will **not** be encrypted.
/// - Name   - The human-readable name of a group. It does not need to be unique and will **not** be encrypted.
/// - Member - A user who is able to encrypt and decrypt data using the group.
/// - Admin  - A user who is able to manage the group's member and admin lists. An admin cannot encrypt or decrypt data using the group
///            unless they first add themselves as group members or are added by another admin.
/// - Owner  - The user who owns the group. The owner has the same permissions as a group admin, but is protected from being removed as
///            a group admin.
/// - Rotation - Changing a group's private key while leaving its public key unchanged. This can be accomplished by calling
///     [group_rotate_private_key](trait.GroupOps.html#tymethod.group_rotate_private_key).

#[async_trait]
pub trait GroupOps {
    /// Creates a group.
    ///
    /// With default `GroupCreateOpts`, the group will be assigned an ID and have no name. The creating user will become the
    /// owner of the group and the only group member and administrator.
    ///
    /// # Arguments
    /// `group_create_opts` - Group creation parameters. Default values are provided with
    ///      [GroupCreateOpts::default()](struct.GroupCreateOpts.html#method.default)
    ///
    /// # Examples
    /// ```
    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
    /// # use ironoxide::prelude::*;
    /// # let sdk: IronOxide = unimplemented!();
    /// # use std::convert::TryFrom;
    /// let group_id = Some(GroupId::try_from("empl412")?);
    /// let opts = GroupCreateOpts::new(group_id, None, true, true, None, vec![], vec![], false);
    /// let group = sdk.group_create(&opts).await?;
    /// # Ok(())
    /// # }
    /// ```
    async fn group_create(&self, group_create_opts: &GroupCreateOpts) -> Result<GroupCreateResult>;

    /// Gets the full metadata for a group.
    ///
    /// The encrypted private key for the group will not be returned.
    ///
    /// # Arguments
    /// - `id` - ID of the group to retrieve
    ///
    /// # Examples
    /// ```
    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
    /// # use ironoxide::prelude::*;
    /// # let sdk: IronOxide = unimplemented!();
    /// # use std::convert::TryFrom;
    /// let group_id = GroupId::try_from("empl412")?;
    /// let group_metadata = sdk.group_get_metadata(&group_id).await?;
    /// # Ok(())
    /// # }
    /// ```
    async fn group_get_metadata(&self, id: &GroupId) -> Result<GroupGetResult>;

    /// Lists all of the groups that the current user is an admin or a member of.
    ///
    /// # Examples
    /// ```
    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
    /// # use ironoxide::prelude::*;
    /// # let sdk: IronOxide = unimplemented!();
    /// let group_list = sdk.group_list().await?;
    /// let groups: Vec<GroupMetaResult> = group_list.result().to_vec();
    /// # Ok(())
    /// # }
    /// ```
    async fn group_list(&self) -> Result<GroupListResult>;

    /// Modifies or removes a group's name.
    ///
    /// Returns the updated metadata of the group.
    ///
    /// # Arguments
    /// - `id` - ID of the group to update
    /// - `name` - New name for the group. Provide a `Some` to update to a new name or a `None` to clear the group's name
    ///
    /// # Examples
    /// ```
    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
    /// # use ironoxide::prelude::*;
    /// # let sdk: IronOxide = unimplemented!();
    /// # use std::convert::TryFrom;
    /// let group_id = GroupId::try_from("empl412")?;
    /// let new_name = GroupName::try_from("HQ Employees")?;
    /// let new_metadata = sdk.group_update_name(&group_id, Some(&new_name)).await?;
    /// # Ok(())
    /// # }
    /// ```
    async fn group_update_name(
        &self,
        id: &GroupId,
        name: Option<&GroupName>,
    ) -> Result<GroupMetaResult>;

    /// Rotates a group's private key while leaving its public key unchanged.
    ///
    /// There's no black magic here! This is accomplished via multi-party computation with the
    /// IronCore webservice.
    ///
    /// Note: You must be an administrator of a group in order to rotate its private key.
    ///
    /// # Arguments
    /// `id` - ID of the group whose private key should be rotated
    ///
    /// # Examples
    /// ```
    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
    /// # use ironoxide::prelude::*;
    /// # let sdk: IronOxide = unimplemented!();
    /// # use std::convert::TryFrom;
    /// let group_id = GroupId::try_from("empl412")?;
    /// let rotate_result = sdk.group_rotate_private_key(&group_id).await?;
    /// let new_rotation = rotate_result.needs_rotation();
    /// # Ok(())
    /// # }
    /// ```
    async fn group_rotate_private_key(&self, id: &GroupId) -> Result<GroupUpdatePrivateKeyResult>;

    /// Adds members to a group.
    ///
    /// Returns successful and failed additions.
    ///
    /// # Arguments
    /// - `id` - ID of the group to add members to
    /// - `users` - List of users to add as group members
    ///
    /// # Examples
    /// ```
    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
    /// # use ironoxide::prelude::*;
    /// # let sdk: IronOxide = unimplemented!();
    /// # use std::convert::TryFrom;
    /// let group_id = GroupId::try_from("empl412")?;
    /// let user = UserId::try_from("colt")?;
    /// let add_result = sdk.group_add_members(&group_id, &vec![user]).await?;
    /// let new_members: Vec<UserId> = add_result.succeeded().to_vec();
    /// let failures: Vec<GroupAccessEditErr> = add_result.failed().to_vec();
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    /// This operation supports partial success. If the request succeeds, then the resulting `GroupAccessEditResult`
    /// will indicate which additions succeeded and which failed, and it will provide an explanation for each failure.
    async fn group_add_members(
        &self,
        id: &GroupId,
        users: &[UserId],
    ) -> Result<GroupAccessEditResult>;

    /// Removes members from a group.
    ///
    /// Returns successful and failed removals.
    ///
    /// # Arguments
    /// - `id` - ID of the group to remove members from
    /// - `revoke_list` - List of users to remove as group members
    ///
    /// # Examples
    /// ```
    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
    /// # use ironoxide::prelude::*;
    /// # let sdk: IronOxide = unimplemented!();
    /// # use std::convert::TryFrom;
    /// let group_id = GroupId::try_from("empl412")?;
    /// let user = UserId::try_from("colt")?;
    /// let remove_result = sdk.group_remove_members(&group_id, &vec![user]).await?;
    /// let removed_members: Vec<UserId> = remove_result.succeeded().to_vec();
    /// let failures: Vec<GroupAccessEditErr> = remove_result.failed().to_vec();
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    /// This operation supports partial success. If the request succeeds, then the resulting `GroupAccessEditResult`
    /// will indicate which removals succeeded and which failed, and it will provide an explanation for each failure.
    async fn group_remove_members(
        &self,
        id: &GroupId,
        revoke_list: &[UserId],
    ) -> Result<GroupAccessEditResult>;

    /// Adds administrators to a group.
    ///
    /// Returns successful and failed additions.
    ///
    /// # Arguments
    /// - `id` - ID of the group to add administrators to
    /// - `users` - List of users to add as group administrators
    ///
    /// # Examples
    /// ```
    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
    /// # use ironoxide::prelude::*;
    /// # let sdk: IronOxide = unimplemented!();
    /// # use std::convert::TryFrom;
    /// let group_id = GroupId::try_from("empl412")?;
    /// let user = UserId::try_from("colt")?;
    /// let add_result = sdk.group_add_admins(&group_id, &vec![user]).await?;
    /// let new_admins: Vec<UserId> = add_result.succeeded().to_vec();
    /// let failures: Vec<GroupAccessEditErr> = add_result.failed().to_vec();
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    /// This operation supports partial success. If the request succeeds, then the resulting `GroupAccessEditResult`
    /// will indicate which additions succeeded and which failed, and it will provide an explanation for each failure.
    async fn group_add_admins(
        &self,
        id: &GroupId,
        users: &[UserId],
    ) -> Result<GroupAccessEditResult>;

    /// Removes administrators from a group.
    ///
    /// Returns successful and failed removals.
    ///
    /// # Arguments
    /// - `id` - ID of the group to remove administrators from
    /// - `revoke_list` - List of users to remove as group administrators
    ///
    /// # Examples
    /// ```
    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
    /// # use ironoxide::prelude::*;
    /// # let sdk: IronOxide = unimplemented!();
    /// # use std::convert::TryFrom;
    /// let group_id = GroupId::try_from("empl412")?;
    /// let user = UserId::try_from("colt")?;
    /// let remove_result = sdk.group_remove_admins(&group_id, &vec![user]).await?;
    /// let removed_admins: Vec<UserId> = remove_result.succeeded().to_vec();
    /// let failures: Vec<GroupAccessEditErr> = remove_result.failed().to_vec();
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    /// This operation supports partial success. If the request succeeds, then the resulting `GroupAccessEditResult`
    /// will indicate which removals succeeded and which failed, and it will provide an explanation for each failure.
    async fn group_remove_admins(
        &self,
        id: &GroupId,
        revoke_list: &[UserId],
    ) -> Result<GroupAccessEditResult>;

    /// Deletes a group.
    ///
    /// A group can be deleted even if it has existing members and administrators.
    ///
    /// **Warning: Deleting a group will prevent its members from decrypting all of the
    /// documents previously encrypted to the group. Caution should be used when deleting groups.**
    ///
    /// # Arguments
    /// `id` - ID of the group to delete
    ///
    /// # Examples
    /// ```
    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
    /// # use ironoxide::prelude::*;
    /// # let sdk: IronOxide = unimplemented!();
    /// # use std::convert::TryFrom;
    /// let group_id = GroupId::try_from("empl412")?;
    /// let deleted_group_id = sdk.group_delete(&group_id).await?;
    /// # Ok(())
    /// # }
    /// ```
    async fn group_delete(&self, id: &GroupId) -> Result<GroupId>;
}

#[async_trait]
impl GroupOps for crate::IronOxide {
    async fn group_create(&self, opts: &GroupCreateOpts) -> Result<GroupCreateResult> {
        let standard_opts = opts.clone().standardize(self.device.auth().account_id())?;
        let all_users = &standard_opts.all_users();
        let GroupCreateOptsStd {
            id,
            name,
            owner,
            admins,
            members,
            needs_rotation,
        } = standard_opts;

        add_optional_timeout(
            group_api::group_create(
                &self.recrypt,
                self.device.auth(),
                id,
                name,
                owner,
                admins,
                members,
                all_users,
                needs_rotation,
            ),
            self.config.sdk_operation_timeout,
            SdkOperation::GroupCreate,
        )
        .await?
    }

    async fn group_get_metadata(&self, id: &GroupId) -> Result<GroupGetResult> {
        add_optional_timeout(
            group_api::get_metadata(self.device.auth(), id),
            self.config.sdk_operation_timeout,
            SdkOperation::GroupGetMetadata,
        )
        .await?
    }

    async fn group_list(&self) -> Result<GroupListResult> {
        add_optional_timeout(
            group_api::list(self.device.auth(), None),
            self.config.sdk_operation_timeout,
            SdkOperation::GroupList,
        )
        .await?
    }

    async fn group_update_name(
        &self,
        id: &GroupId,
        name: Option<&GroupName>,
    ) -> Result<GroupMetaResult> {
        add_optional_timeout(
            group_api::update_group_name(self.device.auth(), id, name),
            self.config.sdk_operation_timeout,
            SdkOperation::GroupUpdateName,
        )
        .await?
    }

    async fn group_rotate_private_key(&self, id: &GroupId) -> Result<GroupUpdatePrivateKeyResult> {
        add_optional_timeout(
            group_api::group_rotate_private_key(
                &self.recrypt,
                self.device().auth(),
                id,
                self.device().device_private_key(),
            ),
            self.config.sdk_operation_timeout,
            SdkOperation::GroupRotatePrivateKey,
        )
        .await?
    }

    async fn group_add_members(
        &self,
        id: &GroupId,
        grant_list: &[UserId],
    ) -> Result<GroupAccessEditResult> {
        add_optional_timeout(
            group_api::group_add_members(
                &self.recrypt,
                self.device.auth(),
                self.device.device_private_key(),
                id,
                &grant_list.to_vec(),
            ),
            self.config.sdk_operation_timeout,
            SdkOperation::GroupAddMembers,
        )
        .await?
    }

    async fn group_remove_members(
        &self,
        id: &GroupId,
        revoke_list: &[UserId],
    ) -> Result<GroupAccessEditResult> {
        add_optional_timeout(
            group_api::group_remove_entity(
                self.device.auth(),
                id,
                &revoke_list.to_vec(),
                group_api::GroupEntity::Member,
            ),
            self.config.sdk_operation_timeout,
            SdkOperation::GroupRemoveMembers,
        )
        .await?
    }

    async fn group_add_admins(
        &self,
        id: &GroupId,
        users: &[UserId],
    ) -> Result<GroupAccessEditResult> {
        add_optional_timeout(
            group_api::group_add_admins(
                &self.recrypt,
                self.device.auth(),
                self.device.device_private_key(),
                id,
                &users.to_vec(),
            ),
            self.config.sdk_operation_timeout,
            SdkOperation::GroupAddAdmins,
        )
        .await?
    }

    async fn group_remove_admins(
        &self,
        id: &GroupId,
        revoke_list: &[UserId],
    ) -> Result<GroupAccessEditResult> {
        add_optional_timeout(
            group_api::group_remove_entity(
                self.device.auth(),
                id,
                &revoke_list.to_vec(),
                group_api::GroupEntity::Admin,
            ),
            self.config.sdk_operation_timeout,
            SdkOperation::GroupRemoveAdmins,
        )
        .await?
    }

    async fn group_delete(&self, id: &GroupId) -> Result<GroupId> {
        add_optional_timeout(
            group_api::group_delete(self.device.auth(), id),
            self.config.sdk_operation_timeout,
            SdkOperation::GroupDelete,
        )
        .await?
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        group::GroupCreateOpts,
        internal::{user_api::UserId, IronOxideErr},
    };

    #[test]
    fn build_group_create_opts_default() {
        let opts = GroupCreateOpts::default();
        assert_eq!(None, opts.id);
        assert_eq!(None, opts.name);
        assert_eq!(true, opts.add_as_member);
    }

    #[test]
    fn group_create_opts_default_standardize() -> Result<(), IronOxideErr> {
        let calling_user_id = UserId::unsafe_from_string("test_user".to_string());
        let opts = GroupCreateOpts::default();
        let std_opts = opts.standardize(&calling_user_id)?;
        assert_eq!(std_opts.all_users(), [calling_user_id.clone()]);
        assert_eq!(std_opts.owner, None);
        assert_eq!(std_opts.admins, [calling_user_id.clone()]);
        assert_eq!(std_opts.members, [calling_user_id]);
        assert_eq!(std_opts.needs_rotation, false);
        Ok(())
    }

    #[test]
    fn group_create_opts_standardize_non_owner() -> Result<(), IronOxideErr> {
        let calling_user_id = UserId::unsafe_from_string("test_user".to_string());
        let owner = UserId::unsafe_from_string("owner".to_string());
        let opts = GroupCreateOpts::new(
            None,
            None,
            false,
            false,
            Some(owner.clone()),
            vec![],
            vec![],
            true,
        );
        let std_opts = opts.standardize(&calling_user_id)?;
        assert_eq!(std_opts.all_users(), [owner.clone()]);
        assert_eq!(std_opts.owner, Some(owner.clone()));
        assert_eq!(std_opts.admins, [owner]);
        assert_eq!(std_opts.members, []);
        assert_eq!(std_opts.needs_rotation, true);
        Ok(())
    }

    #[test]
    fn group_create_opts_standardize_invalid() -> Result<(), IronOxideErr> {
        let calling_user_id = UserId::unsafe_from_string("test_user".to_string());
        let opts = GroupCreateOpts::new(None, None, false, true, None, vec![], vec![], false);
        let std_opts = opts.standardize(&calling_user_id);
        assert!(std_opts.is_err());
        Ok(())
    }
}