1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
pub use crate::internal::group_api::{
    GroupAccessEditErr, GroupAccessEditResult, GroupCreateResult, GroupGetResult, GroupId,
    GroupListResult, GroupMetaResult, GroupName, GroupUpdatePrivateKeyResult,
};
use crate::{
    internal::{group_api, group_api::GroupCreateOptsStd, user_api::UserId, IronOxideErr},
    Result,
};
use vec1::Vec1;

#[derive(Clone)]
/// Options for group creation.
pub struct GroupCreateOpts {
    // unique id of a group within a segment. If none, the server will assign an id.
    id: Option<GroupId>,
    // human readable name of the group. Does not need to be unique.
    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 (in addition to being the group's admin);
    // 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(UserId) - 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
    // note: 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.
    // note: 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 {
    /// Constructor. Also see `default()`
    ///
    /// # Arguments
    /// - `id` - Unique id of a group within a segment. If none, the server will assign an id.
    /// - `name` - Human readable name of the group. Does not need to be unique. Will **not** be encrypted.
    /// - `add_as_admin`
    ///     - true (default) - The creating user will be added as an admin of the group.
    ///     - false - The creating user will not be an admin of the group.
    /// - `add_as_member`
    ///     - true (default) - The creating user will be added as a member of the group.
    ///     - false - The creating user will not be a member of the group.
    /// - `owner` - Specifies the owner of the group
    ///     - 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(UserId) - The provided user will be the owner of the group. This ID will automatically be added to the admins list.
    /// - `admins` - List of users to be added as admins of the group. This list takes priority over `add_as_admin`,
    ///             so the calling user will be added as a member if their id is in this list even if `add_as_admin` is false.
    /// - `members` - 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 their id is in this list even if `add_as_member` is false.
    /// - `needs_rotation`
    ///     - true - group's private key will be marked for rotation
    ///     - false (default) - 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(format!("admins"), format!("admins list cannot be empty"))
        })?;

        if !non_empty_admins.contains(owner_id) {
            Err(IronOxideErr::ValidationError(
                format!("admins"),
                format!("admins list must contain the owner"),
            ))
        } 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 user who calls `group_create()` will be the owner of the group
    /// as well as an admin and member of the group.
    fn default() -> Self {
        GroupCreateOpts::new(None, None, true, true, None, vec![], vec![], false)
    }
}

#[async_trait]
pub trait GroupOps {
    /// List all of the groups that the current user is either an admin or member of.
    ///
    /// # Returns
    /// `GroupListResult` List of (abbreviated) metadata about each group the user is a part of.
    async fn group_list(&self) -> Result<GroupListResult>;

    /// Create a group. The creating user will become a group admin and by default a group member.
    ///
    /// # Arguments
    /// `group_create_opts` - See `GroupCreateOpts`. Use the `Default` implementation for defaults.
    async fn group_create(&self, group_create_opts: &GroupCreateOpts) -> Result<GroupCreateResult>;

    /// Get the full metadata for a specific group given its ID.
    ///
    /// # Arguments
    /// - `id` - Unique ID of the group to retrieve
    ///
    /// # Returns
    /// `GroupMetaResult` with details about the requested group.
    async fn group_get_metadata(&self, id: &GroupId) -> Result<GroupGetResult>;

    /// Delete the identified group. Group does not have to be empty of admins/members in order to
    /// delete the group. **Warning: Deletion of a group will cause all documents encrypted to that
    /// group to no longer be decryptable. Caution should be used when deleting groups.**
    ///
    /// # Arguments
    /// `id` - Unique id of group
    ///
    /// # Returns
    /// Deleted group id or error
    async fn group_delete(&self, id: &GroupId) -> Result<GroupId>;

    /// Update a group name to a new value or clear its value.
    ///
    /// # Arguments
    /// - `id` - ID of the group to update
    /// - `name` - New name for the group. Provide a Some to update to a new name and a None to clear the name field.
    ///
    /// # Returns
    /// `Result<GroupMetaResult>` Metadata about the group that was updated.
    async fn group_update_name(
        &self,
        id: &GroupId,
        name: Option<&GroupName>,
    ) -> Result<GroupMetaResult>;

    /// Add the users as members of a group.
    ///
    /// # Arguments
    /// - `id` - ID of the group to add members to
    /// - `users` - The list of users thet will be added to the group as members.
    /// # Returns
    /// GroupAccessEditResult, which contains all the users that were added. It also contains the users that were not added and
    ///   the reason they were not.
    async fn group_add_members(
        &self,
        id: &GroupId,
        users: &[UserId],
    ) -> Result<GroupAccessEditResult>;

    /// Remove a list of users as members from the group.
    ///
    /// # Arguments
    /// - `id` - ID of the group to remove members from
    /// - `revoke_list` - List of user IDs to remove as members
    ///
    /// # Returns
    /// `Result<GroupAccessEditResult>` List of users that were removed. Also contains the users that failed to be removed
    ///    and the reason they were not.
    async fn group_remove_members(
        &self,
        id: &GroupId,
        revoke_list: &[UserId],
    ) -> Result<GroupAccessEditResult>;

    /// Add the users as admins of a group.
    ///
    /// # Arguments
    /// - `id` - ID of the group to add admins to
    /// - `users` - The list of users that will be added to the group as admins.
    /// # Returns
    /// GroupAccessEditResult, which contains all the users that were added. It also contains the users that were not added and
    ///   the reason they were not.
    async fn group_add_admins(
        &self,
        id: &GroupId,
        users: &[UserId],
    ) -> Result<GroupAccessEditResult>;

    /// Remove a list of users as admins from the group.
    ///
    /// # Arguments
    /// - `id` - ID of the group
    /// - `revoke_list` - List of user IDs to remove as admins
    ///
    /// # Returns
    /// `Result<GroupAccessEditResult>` List of users that were removed. Also contains the users that failed to be removed
    ///    and the reason they were not.
    async fn group_remove_admins(
        &self,
        id: &GroupId,
        revoke_list: &[UserId],
    ) -> Result<GroupAccessEditResult>;

    /// Rotate the provided group's private key, but leave the public key the same.
    /// There's no black magic here! This is accomplished via multi-party computation with the
    /// IronCore webservice.
    /// Note: You must be an admin of the group in order to rotate its private key.
    ///
    /// # Arguments
    /// `id` - ID of the group you wish to rotate the private key of
    ///
    /// # Returns
    /// An indication of whether the group's private key needs an additional rotation
    async fn group_rotate_private_key(&self, id: &GroupId) -> Result<GroupUpdatePrivateKeyResult>;
}

#[async_trait]
impl GroupOps for crate::IronOxide {
    async fn group_list(&self) -> Result<GroupListResult> {
        group_api::list(self.device.auth(), None).await
    }

    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;

        group_api::group_create(
            &self.recrypt,
            self.device.auth(),
            id,
            name,
            owner,
            admins,
            members,
            all_users,
            needs_rotation,
        )
        .await
    }

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

    async fn group_delete(&self, id: &GroupId) -> Result<GroupId> {
        group_api::group_delete(self.device.auth(), id).await
    }

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

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

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

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

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

    async fn group_rotate_private_key(&self, id: &GroupId) -> Result<GroupUpdatePrivateKeyResult> {
        group_api::group_rotate_private_key(
            &self.recrypt,
            self.device().auth(),
            id,
            self.device().device_private_key(),
        )
        .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.clone()]);
        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(())
    }
}