ironoxide/
group.rs

1//! Group API
2//!
3//! See [GroupOps](trait.GroupOps.html) for group functions and key terms.
4
5pub use crate::internal::group_api::{
6    GroupAccessEditErr, GroupAccessEditResult, GroupCreateResult, GroupGetResult, GroupId,
7    GroupListResult, GroupMetaResult, GroupName, GroupUpdatePrivateKeyResult,
8};
9use crate::{
10    common::SdkOperation,
11    internal::{add_optional_timeout, group_api, group_api::GroupCreateOptsStd},
12    user::UserId,
13    IronOxideErr, Result,
14};
15use futures::Future;
16use vec1::Vec1;
17
18/// Options for group creation.
19///
20/// Default values are provided with [GroupCreateOpts::default()](struct.GroupCreateOpts.html#method.default)
21#[derive(Clone, Debug, Eq, Hash, PartialEq)]
22pub struct GroupCreateOpts {
23    /// ID of the group. If `None`, the server will assign the ID.
24    id: Option<GroupId>,
25    /// Name of the group.
26    name: Option<GroupName>,
27    /// - `true` (default) - creating user will be added as an admin of the group.
28    /// - `false` - creating user will not be added as an admin of the group.
29    add_as_admin: bool,
30    /// - `true` (default) - creating user will be added to the group's membership.
31    /// - `false` - creating user will not be added to the group's membership
32    add_as_member: bool,
33    /// 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.
34    /// - `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.
35    /// - `Some` - The provided user will be the owner of the group. This ID will automatically be added to the admins list.
36    owner: Option<UserId>,
37    /// 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.
38    admins: Vec<UserId>,
39    /// 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.
40    members: Vec<UserId>,
41    /// - `true` - group's private key will be marked for rotation
42    /// - `false` (default) - group's private key will not be marked for rotation
43    needs_rotation: bool,
44}
45
46impl GroupCreateOpts {
47    /// # Arguments
48    /// - `id`
49    ///     - `None` (default) - The server will assign the group's ID.
50    ///     - `Some` - The provided ID will be used as the group's ID.
51    /// - `name`
52    ///     - `None` (default) - The group will be created with no name.
53    ///     - `Some` - The provided name will be used as the group's name.
54    /// - `add_as_admin`
55    ///     - `true` (default) - The creating user will be added as a group admin.
56    ///     - `false` - The creating user will not be a group admin.
57    /// - `add_as_member`
58    ///     - `true` (default) - The creating user will be added as a group member.
59    ///     - `false` - The creating user will not be a group member.
60    /// - `owner`
61    ///     - `None` (default) - The creating user will be the owner of the group.
62    ///     - `Some` - The provided user will be the owner of the group. This ID will automatically be added to the admin list.
63    /// - `admins`
64    ///     - The list of users to be added as group admins. This list takes priority over `add_as_admin`,
65    ///       so the calling user will be added as an admin if they are in this list even if `add_as_admin` is false.
66    /// - `members`
67    ///     - The list of users to be added as members of the group. This list takes priority over `add_as_member`,
68    ///       so the calling user will be added as a member if they are in this list even if `add_as_member` is false.
69    /// - `needs_rotation`
70    ///     - `true` - The group's private key will be marked for rotation.
71    ///     - `false` (default) - The group's private key will not be marked for rotation.
72    pub fn new(
73        id: Option<GroupId>,
74        name: Option<GroupName>,
75        add_as_admin: bool,
76        add_as_member: bool,
77        owner: Option<UserId>,
78        admins: Vec<UserId>,
79        members: Vec<UserId>,
80        needs_rotation: bool,
81    ) -> GroupCreateOpts {
82        GroupCreateOpts {
83            id,
84            name,
85            add_as_admin,
86            add_as_member,
87            owner,
88            admins,
89            members,
90            needs_rotation,
91        }
92    }
93
94    fn standardize(self, calling_id: &UserId) -> Result<GroupCreateOptsStd> {
95        // if `add_as_member`, make sure the calling user is in the `members` list
96        let standardized_members = if self.add_as_member && !self.members.contains(calling_id) {
97            let mut members = self.members.clone();
98            members.push(calling_id.clone());
99            members
100        } else {
101            self.members
102        };
103        let (standardized_admins, owner_id) = {
104            // if `add_as_admin`, make sure the calling user is in the `admins` list
105            let mut admins = if self.add_as_admin && !self.admins.contains(calling_id) {
106                let mut admins = self.admins.clone();
107                admins.push(calling_id.clone());
108                admins
109            } else {
110                self.admins
111            };
112            let owner: &UserId = match &self.owner {
113                Some(owner_id) => {
114                    // if the owner is specified, make sure they're in the `admins` list
115                    if !admins.contains(owner_id) {
116                        admins.push(owner_id.clone());
117                    }
118                    owner_id
119                }
120                // if the owner is the calling user (default), they should have been added to the
121                // admins list by `add_as_admin`. If they aren't, it will error later on.
122                None => calling_id,
123            };
124            (admins, owner)
125        };
126
127        let non_empty_admins = Vec1::try_from_vec(standardized_admins).map_err(|_| {
128            IronOxideErr::ValidationError(
129                "admins".to_string(),
130                "admins list cannot be empty".to_string(),
131            )
132        })?;
133
134        if !non_empty_admins.contains(owner_id) {
135            Err(IronOxideErr::ValidationError(
136                "admins".to_string(),
137                "admins list must contain the owner".to_string(),
138            ))
139        } else {
140            Ok(GroupCreateOptsStd {
141                id: self.id,
142                name: self.name,
143                owner: self.owner,
144                admins: non_empty_admins,
145                members: standardized_members,
146                needs_rotation: self.needs_rotation,
147            })
148        }
149    }
150}
151
152impl Default for GroupCreateOpts {
153    /// Default `GroupCreateOpts` for common use cases.
154    ///
155    /// The group will be assigned an ID and have an empty name. The user who calls [group_create](trait.GroupOps.html#tymethod.group_create)
156    /// 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.
157    fn default() -> Self {
158        GroupCreateOpts::new(None, None, true, true, None, vec![], vec![], false)
159    }
160}
161
162/// IronOxide Group Operations
163///
164/// # Key Terms
165/// - ID     - The ID representing a group. It must be unique within the group's segment and will **not** be encrypted.
166/// - Name   - The human-readable name of a group. It does not need to be unique and will **not** be encrypted.
167/// - Member - A user who is able to encrypt and decrypt data using the group.
168/// - 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
169///            unless they first add themselves as group members or are added by another admin.
170/// - Owner  - The user who owns the group. The owner has the same permissions as a group admin, but is protected from being removed as
171///            a group admin.
172/// - Rotation - Changing a group's private key while leaving its public key unchanged. This can be accomplished by calling
173///     [group_rotate_private_key](trait.GroupOps.html#tymethod.group_rotate_private_key).
174
175pub trait GroupOps {
176    /// Creates a group.
177    ///
178    /// With default `GroupCreateOpts`, the group will be assigned an ID and have no name. The creating user will become the
179    /// owner of the group and the only group member and administrator.
180    ///
181    /// # Arguments
182    /// `group_create_opts` - Group creation parameters. Default values are provided with
183    ///      [GroupCreateOpts::default()](struct.GroupCreateOpts.html#method.default)
184    ///
185    /// # Examples
186    /// ```
187    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
188    /// # use ironoxide::prelude::*;
189    /// # let sdk: IronOxide = unimplemented!();
190    /// # use std::convert::TryFrom;
191    /// let group_id = Some(GroupId::try_from("empl412")?);
192    /// let opts = GroupCreateOpts::new(group_id, None, true, true, None, vec![], vec![], false);
193    /// let group = sdk.group_create(&opts).await?;
194    /// # Ok(())
195    /// # }
196    /// ```
197    fn group_create(
198        &self,
199        group_create_opts: &GroupCreateOpts,
200    ) -> impl Future<Output = Result<GroupCreateResult>> + Send;
201
202    /// Gets the full metadata for a group.
203    ///
204    /// The encrypted private key for the group will not be returned.
205    ///
206    /// # Arguments
207    /// - `id` - ID of the group to retrieve
208    ///
209    /// # Examples
210    /// ```
211    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
212    /// # use ironoxide::prelude::*;
213    /// # let sdk: IronOxide = unimplemented!();
214    /// # use std::convert::TryFrom;
215    /// let group_id = GroupId::try_from("empl412")?;
216    /// let group_metadata = sdk.group_get_metadata(&group_id).await?;
217    /// # Ok(())
218    /// # }
219    /// ```
220    fn group_get_metadata(
221        &self,
222        id: &GroupId,
223    ) -> impl Future<Output = Result<GroupGetResult>> + Send;
224
225    /// Lists all of the groups that the current user is an admin or a member of.
226    ///
227    /// # Examples
228    /// ```
229    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
230    /// # use ironoxide::prelude::*;
231    /// # let sdk: IronOxide = unimplemented!();
232    /// let group_list = sdk.group_list().await?;
233    /// let groups: Vec<GroupMetaResult> = group_list.result().to_vec();
234    /// # Ok(())
235    /// # }
236    /// ```
237    fn group_list(&self) -> impl Future<Output = Result<GroupListResult>> + Send;
238
239    /// Modifies or removes a group's name.
240    ///
241    /// Returns the updated metadata of the group.
242    ///
243    /// # Arguments
244    /// - `id` - ID of the group to update
245    /// - `name` - New name for the group. Provide a `Some` to update to a new name or a `None` to clear the group's name
246    ///
247    /// # Examples
248    /// ```
249    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
250    /// # use ironoxide::prelude::*;
251    /// # let sdk: IronOxide = unimplemented!();
252    /// # use std::convert::TryFrom;
253    /// let group_id = GroupId::try_from("empl412")?;
254    /// let new_name = GroupName::try_from("HQ Employees")?;
255    /// let new_metadata = sdk.group_update_name(&group_id, Some(&new_name)).await?;
256    /// # Ok(())
257    /// # }
258    /// ```
259    fn group_update_name(
260        &self,
261        id: &GroupId,
262        name: Option<&GroupName>,
263    ) -> impl Future<Output = Result<GroupMetaResult>> + Send;
264
265    /// Rotates a group's private key while leaving its public key unchanged.
266    ///
267    /// There's no black magic here! This is accomplished via multi-party computation with the
268    /// IronCore webservice.
269    ///
270    /// Note: You must be an administrator of a group in order to rotate its private key.
271    ///
272    /// # Arguments
273    /// `id` - ID of the group whose private key should be rotated
274    ///
275    /// # Examples
276    /// ```
277    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
278    /// # use ironoxide::prelude::*;
279    /// # let sdk: IronOxide = unimplemented!();
280    /// # use std::convert::TryFrom;
281    /// let group_id = GroupId::try_from("empl412")?;
282    /// let rotate_result = sdk.group_rotate_private_key(&group_id).await?;
283    /// let new_rotation = rotate_result.needs_rotation();
284    /// # Ok(())
285    /// # }
286    /// ```
287    fn group_rotate_private_key(
288        &self,
289        id: &GroupId,
290    ) -> impl Future<Output = Result<GroupUpdatePrivateKeyResult>> + Send;
291
292    /// Adds members to a group.
293    ///
294    /// Returns successful and failed additions.
295    ///
296    /// # Arguments
297    /// - `id` - ID of the group to add members to
298    /// - `users` - List of users to add as group members
299    ///
300    /// # Examples
301    /// ```
302    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
303    /// # use ironoxide::prelude::*;
304    /// # let sdk: IronOxide = unimplemented!();
305    /// # use std::convert::TryFrom;
306    /// let group_id = GroupId::try_from("empl412")?;
307    /// let user = UserId::try_from("colt")?;
308    /// let add_result = sdk.group_add_members(&group_id, &vec![user]).await?;
309    /// let new_members: Vec<UserId> = add_result.succeeded().to_vec();
310    /// let failures: Vec<GroupAccessEditErr> = add_result.failed().to_vec();
311    /// # Ok(())
312    /// # }
313    /// ```
314    ///
315    /// # Errors
316    /// This operation supports partial success. If the request succeeds, then the resulting `GroupAccessEditResult`
317    /// will indicate which additions succeeded and which failed, and it will provide an explanation for each failure.
318    fn group_add_members(
319        &self,
320        id: &GroupId,
321        users: &[UserId],
322    ) -> impl Future<Output = Result<GroupAccessEditResult>> + Send;
323
324    /// Removes members from a group.
325    ///
326    /// Returns successful and failed removals.
327    ///
328    /// # Arguments
329    /// - `id` - ID of the group to remove members from
330    /// - `revoke_list` - List of users to remove as group members
331    ///
332    /// # Examples
333    /// ```
334    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
335    /// # use ironoxide::prelude::*;
336    /// # let sdk: IronOxide = unimplemented!();
337    /// # use std::convert::TryFrom;
338    /// let group_id = GroupId::try_from("empl412")?;
339    /// let user = UserId::try_from("colt")?;
340    /// let remove_result = sdk.group_remove_members(&group_id, &vec![user]).await?;
341    /// let removed_members: Vec<UserId> = remove_result.succeeded().to_vec();
342    /// let failures: Vec<GroupAccessEditErr> = remove_result.failed().to_vec();
343    /// # Ok(())
344    /// # }
345    /// ```
346    ///
347    /// # Errors
348    /// This operation supports partial success. If the request succeeds, then the resulting `GroupAccessEditResult`
349    /// will indicate which removals succeeded and which failed, and it will provide an explanation for each failure.
350    fn group_remove_members(
351        &self,
352        id: &GroupId,
353        revoke_list: &[UserId],
354    ) -> impl Future<Output = Result<GroupAccessEditResult>> + Send;
355
356    /// Adds administrators to a group.
357    ///
358    /// Returns successful and failed additions.
359    ///
360    /// # Arguments
361    /// - `id` - ID of the group to add administrators to
362    /// - `users` - List of users to add as group administrators
363    ///
364    /// # Examples
365    /// ```
366    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
367    /// # use ironoxide::prelude::*;
368    /// # let sdk: IronOxide = unimplemented!();
369    /// # use std::convert::TryFrom;
370    /// let group_id = GroupId::try_from("empl412")?;
371    /// let user = UserId::try_from("colt")?;
372    /// let add_result = sdk.group_add_admins(&group_id, &vec![user]).await?;
373    /// let new_admins: Vec<UserId> = add_result.succeeded().to_vec();
374    /// let failures: Vec<GroupAccessEditErr> = add_result.failed().to_vec();
375    /// # Ok(())
376    /// # }
377    /// ```
378    ///
379    /// # Errors
380    /// This operation supports partial success. If the request succeeds, then the resulting `GroupAccessEditResult`
381    /// will indicate which additions succeeded and which failed, and it will provide an explanation for each failure.
382    fn group_add_admins(
383        &self,
384        id: &GroupId,
385        users: &[UserId],
386    ) -> impl Future<Output = Result<GroupAccessEditResult>> + Send;
387
388    /// Removes administrators from a group.
389    ///
390    /// Returns successful and failed removals.
391    ///
392    /// # Arguments
393    /// - `id` - ID of the group to remove administrators from
394    /// - `revoke_list` - List of users to remove as group administrators
395    ///
396    /// # Examples
397    /// ```
398    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
399    /// # use ironoxide::prelude::*;
400    /// # let sdk: IronOxide = unimplemented!();
401    /// # use std::convert::TryFrom;
402    /// let group_id = GroupId::try_from("empl412")?;
403    /// let user = UserId::try_from("colt")?;
404    /// let remove_result = sdk.group_remove_admins(&group_id, &vec![user]).await?;
405    /// let removed_admins: Vec<UserId> = remove_result.succeeded().to_vec();
406    /// let failures: Vec<GroupAccessEditErr> = remove_result.failed().to_vec();
407    /// # Ok(())
408    /// # }
409    /// ```
410    ///
411    /// # Errors
412    /// This operation supports partial success. If the request succeeds, then the resulting `GroupAccessEditResult`
413    /// will indicate which removals succeeded and which failed, and it will provide an explanation for each failure.
414    fn group_remove_admins(
415        &self,
416        id: &GroupId,
417        revoke_list: &[UserId],
418    ) -> impl Future<Output = Result<GroupAccessEditResult>> + Send;
419
420    /// Deletes a group.
421    ///
422    /// A group can be deleted even if it has existing members and administrators.
423    ///
424    /// **Warning: Deleting a group will prevent its members from decrypting all of the
425    /// documents previously encrypted to the group. Caution should be used when deleting groups.**
426    ///
427    /// # Arguments
428    /// `id` - ID of the group to delete
429    ///
430    /// # Examples
431    /// ```
432    /// # async fn run() -> Result<(), ironoxide::IronOxideErr> {
433    /// # use ironoxide::prelude::*;
434    /// # let sdk: IronOxide = unimplemented!();
435    /// # use std::convert::TryFrom;
436    /// let group_id = GroupId::try_from("empl412")?;
437    /// let deleted_group_id = sdk.group_delete(&group_id).await?;
438    /// # Ok(())
439    /// # }
440    /// ```
441    fn group_delete(&self, id: &GroupId) -> impl Future<Output = Result<GroupId>> + Send;
442}
443
444impl GroupOps for crate::IronOxide {
445    async fn group_create(&self, opts: &GroupCreateOpts) -> Result<GroupCreateResult> {
446        let standard_opts = opts.clone().standardize(self.device.auth().account_id())?;
447        let all_users = &standard_opts.all_users();
448        let GroupCreateOptsStd {
449            id,
450            name,
451            owner,
452            admins,
453            members,
454            needs_rotation,
455        } = standard_opts;
456
457        add_optional_timeout(
458            group_api::group_create(
459                &self.recrypt,
460                self.device.auth(),
461                id,
462                name,
463                owner,
464                admins,
465                members,
466                all_users,
467                needs_rotation,
468            ),
469            self.config.sdk_operation_timeout,
470            SdkOperation::GroupCreate,
471        )
472        .await?
473    }
474
475    async fn group_get_metadata(&self, id: &GroupId) -> Result<GroupGetResult> {
476        add_optional_timeout(
477            group_api::get_metadata(self.device.auth(), id),
478            self.config.sdk_operation_timeout,
479            SdkOperation::GroupGetMetadata,
480        )
481        .await?
482    }
483
484    async fn group_list(&self) -> Result<GroupListResult> {
485        add_optional_timeout(
486            group_api::list(self.device.auth(), None),
487            self.config.sdk_operation_timeout,
488            SdkOperation::GroupList,
489        )
490        .await?
491    }
492
493    async fn group_update_name(
494        &self,
495        id: &GroupId,
496        name: Option<&GroupName>,
497    ) -> Result<GroupMetaResult> {
498        add_optional_timeout(
499            group_api::update_group_name(self.device.auth(), id, name),
500            self.config.sdk_operation_timeout,
501            SdkOperation::GroupUpdateName,
502        )
503        .await?
504    }
505
506    async fn group_rotate_private_key(&self, id: &GroupId) -> Result<GroupUpdatePrivateKeyResult> {
507        add_optional_timeout(
508            group_api::group_rotate_private_key(
509                &self.recrypt,
510                self.device().auth(),
511                id,
512                self.device().device_private_key(),
513            ),
514            self.config.sdk_operation_timeout,
515            SdkOperation::GroupRotatePrivateKey,
516        )
517        .await?
518    }
519
520    async fn group_add_members(
521        &self,
522        id: &GroupId,
523        grant_list: &[UserId],
524    ) -> Result<GroupAccessEditResult> {
525        add_optional_timeout(
526            group_api::group_add_members(
527                &self.recrypt,
528                self.device.auth(),
529                self.device.device_private_key(),
530                id,
531                grant_list,
532            ),
533            self.config.sdk_operation_timeout,
534            SdkOperation::GroupAddMembers,
535        )
536        .await?
537    }
538
539    async fn group_remove_members(
540        &self,
541        id: &GroupId,
542        revoke_list: &[UserId],
543    ) -> Result<GroupAccessEditResult> {
544        add_optional_timeout(
545            group_api::group_remove_entity(
546                self.device.auth(),
547                id,
548                revoke_list,
549                group_api::GroupEntity::Member,
550            ),
551            self.config.sdk_operation_timeout,
552            SdkOperation::GroupRemoveMembers,
553        )
554        .await?
555    }
556
557    async fn group_add_admins(
558        &self,
559        id: &GroupId,
560        users: &[UserId],
561    ) -> Result<GroupAccessEditResult> {
562        add_optional_timeout(
563            group_api::group_add_admins(
564                &self.recrypt,
565                self.device.auth(),
566                self.device.device_private_key(),
567                id,
568                users,
569            ),
570            self.config.sdk_operation_timeout,
571            SdkOperation::GroupAddAdmins,
572        )
573        .await?
574    }
575
576    async fn group_remove_admins(
577        &self,
578        id: &GroupId,
579        revoke_list: &[UserId],
580    ) -> Result<GroupAccessEditResult> {
581        add_optional_timeout(
582            group_api::group_remove_entity(
583                self.device.auth(),
584                id,
585                revoke_list,
586                group_api::GroupEntity::Admin,
587            ),
588            self.config.sdk_operation_timeout,
589            SdkOperation::GroupRemoveAdmins,
590        )
591        .await?
592    }
593
594    async fn group_delete(&self, id: &GroupId) -> Result<GroupId> {
595        add_optional_timeout(
596            group_api::group_delete(self.device.auth(), id),
597            self.config.sdk_operation_timeout,
598            SdkOperation::GroupDelete,
599        )
600        .await?
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use crate::{
607        group::GroupCreateOpts,
608        internal::{user_api::UserId, IronOxideErr},
609    };
610
611    #[test]
612    fn build_group_create_opts_default() {
613        let opts = GroupCreateOpts::default();
614        assert_eq!(None, opts.id);
615        assert_eq!(None, opts.name);
616        assert!(opts.add_as_member);
617    }
618
619    #[test]
620    fn group_create_opts_default_standardize() -> Result<(), IronOxideErr> {
621        let calling_user_id = UserId::unsafe_from_string("test_user".to_string());
622        let opts = GroupCreateOpts::default();
623        let std_opts = opts.standardize(&calling_user_id)?;
624        assert_eq!(std_opts.all_users(), [calling_user_id.clone()]);
625        assert_eq!(std_opts.owner, None);
626        assert_eq!(std_opts.admins, [calling_user_id.clone()]);
627        assert_eq!(std_opts.members, [calling_user_id]);
628        assert!(!std_opts.needs_rotation);
629        Ok(())
630    }
631
632    #[test]
633    fn group_create_opts_standardize_non_owner() -> Result<(), IronOxideErr> {
634        let calling_user_id = UserId::unsafe_from_string("test_user".to_string());
635        let owner = UserId::unsafe_from_string("owner".to_string());
636        let opts = GroupCreateOpts::new(
637            None,
638            None,
639            false,
640            false,
641            Some(owner.clone()),
642            vec![],
643            vec![],
644            true,
645        );
646        let std_opts = opts.standardize(&calling_user_id)?;
647        assert_eq!(std_opts.all_users(), [owner.clone()]);
648        assert_eq!(std_opts.owner, Some(owner.clone()));
649        assert_eq!(std_opts.admins, [owner]);
650        assert_eq!(std_opts.members, []);
651        assert!(std_opts.needs_rotation);
652        Ok(())
653    }
654
655    #[test]
656    fn group_create_opts_standardize_invalid() -> Result<(), IronOxideErr> {
657        let calling_user_id = UserId::unsafe_from_string("test_user".to_string());
658        let opts = GroupCreateOpts::new(None, None, false, true, None, vec![], vec![], false);
659        let std_opts = opts.standardize(&calling_user_id);
660        assert!(std_opts.is_err());
661        Ok(())
662    }
663}