diff --git a/helper/group.go b/helper/group.go new file mode 100644 index 0000000..a32b6e5 --- /dev/null +++ b/helper/group.go @@ -0,0 +1,186 @@ +package helper + +import ( + "context" + "fmt" + + "github.com/speatzle/go-passbolt/api" +) + +// GroupMembershipOperation creates/modifies/deletes a group membership +type GroupMembershipOperation struct { + GroupMembership + Delete bool +} + +// GroupMembership containes who and what kind of membership they have with a group +type GroupMembership struct { + UserID string + IsGroupManager bool +} + +// CreateGroup creates a Groups with Name and Memberships +func CreateGroup(ctx context.Context, c *api.Client, name string, operations []GroupMembershipOperation) (string, error) { + memberships := []api.GroupMembership{} + for _, o := range operations { + if o.Delete { + return "", fmt.Errorf("Cannot Delete Membership during Group Creation") + } + memberships = append(memberships, api.GroupMembership{ + UserID: o.UserID, + IsAdmin: o.IsGroupManager, + }) + } + group, err := c.CreateGroup(ctx, api.Group{ + Name: name, + GroupUsers: memberships, + }) + if err != nil { + return "", fmt.Errorf("Creating Group: %w", err) + } + return group.ID, nil +} + +// GetGroup gets a Groups Name and Memberships +func GetGroup(ctx context.Context, c *api.Client, groupID string) (string, []GroupMembership, error) { + // for some reason the groups index api call does not give back the groups_users even though it is supposed to, so i have to do this... + groups, err := c.GetGroups(ctx, &api.GetGroupsOptions{ + ContainGroupUser: true, + }) + if err != nil { + return "", nil, fmt.Errorf("Getting Groups: %w", err) + } + + for _, g := range groups { + if g.ID == groupID { + memberships := []GroupMembership{} + for _, m := range g.GroupUsers { + memberships = append(memberships, GroupMembership{ + UserID: m.UserID, + IsGroupManager: m.IsAdmin, + }) + } + return g.Name, memberships, nil + } + } + return "", nil, fmt.Errorf("Cannot Find Group in API Response") +} + +// UpdateGroup Updates a Groups Name and Memberships +func UpdateGroup(ctx context.Context, c *api.Client, groupID, name string, operations []GroupMembershipOperation) error { + // for some reason the groups index api call does not give back the groups_users even though it is supposed to, so i have to do this... + groups, err := c.GetGroups(ctx, &api.GetGroupsOptions{ + ContainGroupUser: true, + }) + if err != nil { + return fmt.Errorf("Getting Groups: %w", err) + } + + var currentMemberships []api.GroupMembership + for _, g := range groups { + if g.ID == groupID { + currentMemberships = g.GroupUsers + break + } + } + if currentMemberships == nil { + return fmt.Errorf("Cannot Find Group with ID %v", groupID) + } + + request := api.GroupUpdate{ + Name: name, + GroupChanges: []api.GroupMembership{}, + Secrets: []api.Secret{}, + } + + // Generate Group Membership changes based on current Group Memberships + for _, operation := range operations { + membership, err := getMembershipByUserID(currentMemberships, operation.UserID) + if err != nil { + // Membership does not Exist so we can only create a new one + if operation.Delete { + return fmt.Errorf("Cannot Delete User %v as it has no membership", operation.UserID) + } + request.GroupChanges = append(request.GroupChanges, api.GroupMembership{ + UserID: operation.UserID, + IsAdmin: operation.IsGroupManager, + }) + } else { + // Membership Exists so we can modify or delete it + request.GroupChanges = append(request.GroupChanges, api.GroupMembership{ + ID: membership.ID, + IsAdmin: operation.IsGroupManager, + Delete: operation.Delete, + }) + } + } + + dryrun, err := c.UpdateGroupDryRun(ctx, groupID, request) + if err != nil { + return fmt.Errorf("Update Group Dryrun: %w", err) + } + + var users []api.User + // We can skip Getting users if we don't need to reencrypt any secrets + if len(dryrun.DryRun.SecretsNeeded) != 0 { + users, err = c.GetUsers(ctx, &api.GetUsersOptions{}) + if err != nil { + return fmt.Errorf("Getting Users: %w", err) + } + } + + // The API gives it back nested so we just put it into a list here + secrets := []api.Secret{} + for _, container := range dryrun.DryRun.Secrets { + secrets = append(secrets, container.Secret...) + } + + decryptedSecretCache := map[string]string{} + for _, container := range dryrun.DryRun.SecretsNeeded { + missingSecret := container.Secret + // Deduplicate Secret Decrypting for when adding multiple users to a group + if decryptedSecretCache[missingSecret.ResourceID] == "" { + secret, err := getSecretByResourceID(secrets, missingSecret.ResourceID) + if err != nil { + return fmt.Errorf("Get Secret from Dryrun Response: %w", err) + } + + msg, err := c.DecryptMessage(secret.Data) + if err != nil { + return fmt.Errorf("Decrypting Secret: %w", err) + } + + decryptedSecretCache[missingSecret.ResourceID] = msg + } + + pubkey, err := getPublicKeyByUserID(missingSecret.UserID, users) + if err != nil { + return fmt.Errorf("Get pubkey for User: %w", err) + } + + newSecretData, err := c.EncryptMessageWithPublicKey(pubkey, decryptedSecretCache[missingSecret.ResourceID]) + if err != nil { + return fmt.Errorf("Encrypting Secret: %w", err) + } + request.Secrets = append(request.Secrets, api.Secret{ + UserID: missingSecret.UserID, + ResourceID: missingSecret.ResourceID, + Data: newSecretData, + }) + } + + _, err = c.UpdateGroup(ctx, groupID, request) + if err != nil { + return fmt.Errorf("Updating Group: %w", err) + } + return nil +} + +// DeleteGroup Deletes a Group +func DeleteGroup(ctx context.Context, c *api.Client, groupID string) error { + err := c.DeleteGroup(ctx, groupID) + if err != nil { + return fmt.Errorf("Deleting Group: %w", err) + } + return nil +}