diff --git a/internal/iam/repository_principal_role.go b/internal/iam/repository_principal_role.go index 692e5f1215..0701b1a574 100644 --- a/internal/iam/repository_principal_role.go +++ b/internal/iam/repository_principal_role.go @@ -110,25 +110,250 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV return principalRoles, nil } -func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, userIds, groupIds []string, opt ...Option) ([]PrincipalRole, int, error) { - panic("not implemented") -} +// tests: no change, just delete, just add -// ListPrincipalRoles returns the principal roles for the roleId and supports the WithLimit option. -func (r *Repository) ListPrincipalRoles(ctx context.Context, roleId string, opt ...Option) ([]PrincipalRole, error) { +func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleVersion int, userIds, groupIds []string, opt ...Option) ([]PrincipalRole, int, error) { if roleId == "" { - return nil, fmt.Errorf("lookup principal roles: missing role id %w", db.ErrInvalidParameter) + return nil, db.NoRowsAffected, fmt.Errorf("set principal roles: missing role id: %w", db.ErrInvalidParameter) } - var roles []principalRoleView - if err := r.list(ctx, &roles, "role_id = ?", []interface{}{roleId}, opt...); err != nil { - return nil, fmt.Errorf("lookup principal role: unable to lookup roles: %w", err) + if len(userIds) == 0 && len(groupIds) == 0 { + return nil, db.NoRowsAffected, fmt.Errorf("set principal roles: missing either user or groups to delete %w", db.ErrInvalidParameter) } - principals := make([]PrincipalRole, 0, len(roles)) - for _, r := range roles { - principals = append(principals, r) + toSet, err := r.principalsToSet(ctx, roleId, userIds, groupIds) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set principal roles: unable to determine set: %w", err) } - return principals, nil + role := allocRole() + role.PublicId = roleId + scope, err := role.GetScope(ctx, r.reader) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set principal roles: unable to get role %s scope: %w", roleId, err) + } + // handle no change to existing principal roles + if len(toSet.addUserRoles) == 0 && len(toSet.addGroupRoles) == 0 && len(toSet.deleteUserRoles) == 0 && len(toSet.deleteGroupRoles) == 0 { + results := make([]PrincipalRole, 0, len(userIds)+len(groupIds)) + for _, id := range userIds { + role, err := NewUserRole(scope.PublicId, roleId, id) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("add principal roles: unable to create in memory user role: %w", err) + } + results = append(results, role) + } + for _, id := range groupIds { + role, err := NewGroupRole(scope.PublicId, roleId, id) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("add principal roles: unable to create in memory group role: %w", err) + } + results = append(results, role) + } + return results, db.NoRowsAffected, nil + } + + var currentPrincipals []PrincipalRole + var totalRowsAffected int + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + // we need a roleTicket, which won't be redeemed until all the other + // writes are successful. We can't just use a single ticket because + // we need to write oplog entries for deletes and adds + roleTicket, err := w.GetTicket(role) + if err != nil { + return fmt.Errorf("set principal roles: unable to get ticket for role: %w", err) + } + updatedRole := allocRole() + updatedRole.PublicId = roleId + updatedRole.Version = uint32(roleVersion) + 1 + var roleOplogMsg oplog.Message + rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(roleVersion)) + if err != nil { + return fmt.Errorf("set principal roles: unable to update role version: %w", err) + } + if rowsUpdated != 1 { + return fmt.Errorf("set principal roles: updated role and %d rows updated", rowsUpdated) + } + if len(toSet.deleteUserRoles) > 0 || len(toSet.deleteGroupRoles) > 0 { + msgs := make([]*oplog.Message, 0, 2) + // deleteTicket which will be redeemed when we write the oplog + // for these deletes + deleteTicket, err := w.GetTicket(&principalRoleView{}) + if err != nil { + return fmt.Errorf("set principal roles: unable to get ticket for principal role deletes: %w", err) + } + if len(toSet.deleteUserRoles) > 0 { + userOplogMsgs := make([]*oplog.Message, 0, len(toSet.deleteUserRoles)) + rowsDeleted, err := w.DeleteItems(ctx, toSet.deleteUserRoles, db.NewOplogMsgs(&userOplogMsgs)) + if err != nil { + return fmt.Errorf("set principal roles: unable to delete user roles: %w", err) + } + if rowsDeleted != len(toSet.deleteUserRoles) { + return fmt.Errorf("set principal roles: user roles deleted %d did not match request for %d", rowsDeleted, len(toSet.deleteUserRoles)) + } + totalRowsAffected += rowsDeleted + msgs = append(msgs, userOplogMsgs...) + } + if len(toSet.deleteGroupRoles) > 0 { + grpOplogMsgs := make([]*oplog.Message, 0, len(toSet.deleteGroupRoles)) + rowsDeleted, err := w.DeleteItems(ctx, toSet.deleteGroupRoles, db.NewOplogMsgs(&grpOplogMsgs)) + if err != nil { + return fmt.Errorf("set principal roles: unable to delete groups: %w", err) + } + if rowsDeleted != len(toSet.deleteGroupRoles) { + return fmt.Errorf("set principal roles: group roles deleted %d did not match request for %d", rowsDeleted, len(toSet.deleteGroupRoles)) + } + totalRowsAffected += rowsDeleted + msgs = append(msgs, grpOplogMsgs...) + } + + metadata := oplog.Metadata{ + "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()}, + "scope-id": []string{scope.PublicId}, + "scope-type": []string{scope.Type}, + "resource-public-id": []string{roleId}, + } + // write the oplog msgs for the deletes + if err := w.WriteOplogEntryWith(ctx, r.wrapper, deleteTicket, metadata, msgs); err != nil { + return fmt.Errorf("set principal roles: unable to write oplog for deletes: %w", err) + } + } + if len(toSet.addUserRoles) > 0 || len(toSet.addGroupRoles) > 0 { + msgs := make([]*oplog.Message, 0, 2) + // addTicket which will be redeemed when we write the oplog + // entry for these writes. + addTicket, err := w.GetTicket(&principalRoleView{}) + if err != nil { + return fmt.Errorf("set principal roles: unable to get ticket for principal role additions: %w", err) + } + if len(toSet.addUserRoles) > 0 { + userOplogMsgs := make([]*oplog.Message, 0, len(toSet.addUserRoles)) + if err := w.CreateItems(ctx, toSet.addUserRoles, db.NewOplogMsgs(&userOplogMsgs)); err != nil { + return fmt.Errorf("set principal roles: unable to add users: %w", err) + } + msgs = append(msgs, userOplogMsgs...) + } + if len(toSet.addGroupRoles) > 0 { + grpOplogMsgs := make([]*oplog.Message, 0, len(toSet.addGroupRoles)) + if err := w.CreateItems(ctx, toSet.addGroupRoles, db.NewOplogMsgs(&grpOplogMsgs)); err != nil { + return fmt.Errorf("set principal roles: unable to add groups: %w", err) + } + msgs = append(msgs, grpOplogMsgs...) + } + metadata := oplog.Metadata{ + "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, + "scope-id": []string{scope.PublicId}, + "scope-type": []string{scope.Type}, + "resource-public-id": []string{roleId}, + } + // write the oplog msgs for the additions + if err := w.WriteOplogEntryWith(ctx, r.wrapper, addTicket, metadata, msgs); err != nil { + return fmt.Errorf("set principal roles: unable to write oplog for additions: %w", err) + } + } + // we're done with all the principal writes, so let's write the + // role's update oplog message + metadata := oplog.Metadata{ + "op-type": []string{oplog.OpType_OP_TYPE_UPDATE.String()}, + "scope-id": []string{scope.PublicId}, + "scope-type": []string{scope.Type}, + "resource-public-id": []string{roleId}, + } + if err := w.WriteOplogEntryWith(ctx, r.wrapper, roleTicket, metadata, []*oplog.Message{&roleOplogMsg}); err != nil { + return fmt.Errorf("set principal roles: unable to write oplog for additions: %w", err) + } + + currentPrincipals, err = r.ListPrincipalRoles(ctx, roleId) + if err != nil { + return fmt.Errorf("set principal roles: unable to retrieve current principal roles after sets: %w", err) + } + return nil + }) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set principal roles: unable to set principals: %w", err) + } + return currentPrincipals, totalRowsAffected, nil +} + +type principalSet struct { + addUserRoles []interface{} + addGroupRoles []interface{} + deleteUserRoles []interface{} + deleteGroupRoles []interface{} +} + +func (r *Repository) principalsToSet(ctx context.Context, roleId string, userIds, groupIds []string) (*principalSet, error) { + existing, err := r.ListPrincipalRoles(ctx, roleId) + if err != nil { + return nil, fmt.Errorf("unable to list existing principal role %s: %w", roleId, err) + } + existingUsers := map[string]PrincipalRole{} + existingGroups := map[string]PrincipalRole{} + for _, p := range existing { + switch p.GetType() { + case UserRoleType.String(): + existingUsers[p.GetPrincipalId()] = p + case GroupRoleType.String(): + existingGroups[p.GetPrincipalId()] = p + default: + return nil, fmt.Errorf("%s is unknown principal type %s", p.GetPrincipalId(), p.GetType()) + } + } + var newUserRoles []interface{} + userIdsMap := map[string]struct{}{} + for _, id := range userIds { + userIdsMap[id] = struct{}{} + if p, ok := existingUsers[id]; !ok { + usrRole, err := NewUserRole(p.GetScopeId(), p.GetRoleId(), id) + if err != nil { + return nil, fmt.Errorf("unable to create in memory user role for add: %w", err) + } + newUserRoles = append(newUserRoles, usrRole) + } + } + var newGrpRoles []interface{} + groupIdsMap := map[string]struct{}{} + for _, id := range groupIds { + groupIdsMap[id] = struct{}{} + if p, ok := existingGroups[id]; !ok { + grpRole, err := NewGroupRole(p.GetScopeId(), p.GetRoleId(), id) + if err != nil { + return nil, fmt.Errorf("unable to create in memory group role for add: %w", err) + } + newGrpRoles = append(newGrpRoles, grpRole) + } + } + var deleteUserRoles []interface{} + for _, p := range existingUsers { + if _, ok := userIdsMap[p.GetPrincipalId()]; !ok { + usrRole, err := NewUserRole(p.GetScopeId(), p.GetRoleId(), p.GetPrincipalId()) + if err != nil { + return nil, fmt.Errorf("unable to create in memory user role for delete: %w", err) + } + deleteUserRoles = append(deleteUserRoles, usrRole) + } + } + var deleteGrpRoles []interface{} + for _, p := range existingGroups { + if _, ok := userIdsMap[p.GetPrincipalId()]; !ok { + grpRole, err := NewGroupRole(p.GetScopeId(), p.GetRoleId(), p.GetPrincipalId()) + if err != nil { + return nil, fmt.Errorf("unable to create in memory group role for delete: %w", err) + } + deleteGrpRoles = append(deleteGrpRoles, grpRole) + } + } + return &principalSet{ + addUserRoles: newUserRoles, + addGroupRoles: newGrpRoles, + deleteUserRoles: deleteUserRoles, + deleteGroupRoles: deleteGrpRoles, + }, nil } + +// DeletePrincipalRoles principals (userIds and/or groupIds) from a role +// (roleId). The role's current db version must match the roleVersion or an +// error will be returned. func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, roleVersion int, userIds, groupIds []string, opt ...Option) (int, error) { if roleId == "" { return db.NoRowsAffected, fmt.Errorf("delete principal roles: missing role id: %w", db.ErrInvalidParameter) @@ -219,7 +444,23 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro }, ) if err != nil { - return db.NoRowsAffected, fmt.Errorf("delete principal roles: error deleting roles: %w", err) + return db.NoRowsAffected, fmt.Errorf("delete principal roles: error deleting principal roles: %w", err) } return totalRowsDeleted, nil } + +// ListPrincipalRoles returns the principal roles for the roleId and supports the WithLimit option. +func (r *Repository) ListPrincipalRoles(ctx context.Context, roleId string, opt ...Option) ([]PrincipalRole, error) { + if roleId == "" { + return nil, fmt.Errorf("lookup principal roles: missing role id %w", db.ErrInvalidParameter) + } + var roles []principalRoleView + if err := r.list(ctx, &roles, "role_id = ?", []interface{}{roleId}, opt...); err != nil { + return nil, fmt.Errorf("lookup principal role: unable to lookup roles: %w", err) + } + principals := make([]PrincipalRole, 0, len(roles)) + for _, r := range roles { + principals = append(principals, r) + } + return principals, nil +}