Enforce at least one owner
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -21,6 +21,8 @@ import (
|
|||||||
|
|
||||||
type MembershipFilter struct {
|
type MembershipFilter struct {
|
||||||
email *mail.Addr
|
email *mail.Addr
|
||||||
|
role *MembershipRole
|
||||||
|
state *MembershipState
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMembershipFilter() *MembershipFilter {
|
func NewMembershipFilter() *MembershipFilter {
|
||||||
@@ -36,9 +38,29 @@ func (f *MembershipFilter) Email() *mail.Addr {
|
|||||||
return f.email
|
return f.email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *MembershipFilter) WithRole(role MembershipRole) *MembershipFilter {
|
||||||
|
f.role = &role
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MembershipFilter) Role() *MembershipRole {
|
||||||
|
return f.role
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MembershipFilter) WithState(state MembershipState) *MembershipFilter {
|
||||||
|
f.state = &state
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MembershipFilter) State() *MembershipState {
|
||||||
|
return f.state
|
||||||
|
}
|
||||||
|
|
||||||
func (f *MembershipFilter) SQLArguments() pgx.StrictNamedArgs {
|
func (f *MembershipFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||||
return pgx.StrictNamedArgs{
|
return pgx.StrictNamedArgs{
|
||||||
"filter_email": f.email,
|
"filter_email": f.email,
|
||||||
|
"filter_role": f.role,
|
||||||
|
"filter_state": f.state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,5 +72,19 @@ func (f *MembershipFilter) SQLFragment() string {
|
|||||||
i.email_address = @filter_email::text
|
i.email_address = @filter_email::text
|
||||||
ELSE TRUE
|
ELSE TRUE
|
||||||
END
|
END
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
CASE
|
||||||
|
WHEN @filter_role::text IS NOT NULL THEN
|
||||||
|
m.role = @filter_role::authz_role
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
CASE
|
||||||
|
WHEN @filter_state::text IS NOT NULL THEN
|
||||||
|
m.state = @filter_state::membership_state
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
)`
|
)`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,18 @@ func (e ErrMembershipManagedBySCIM) Error() string {
|
|||||||
return fmt.Sprintf("membership %q is managed by SCIM and cannot be deleted manually", e.MembershipID)
|
return fmt.Sprintf("membership %q is managed by SCIM and cannot be deleted manually", e.MembershipID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrLastActiveOwner struct {
|
||||||
|
MembershipID gid.GID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLastActiveOwnerError(membershipID gid.GID) error {
|
||||||
|
return &ErrLastActiveOwner{MembershipID: membershipID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrLastActiveOwner) Error() string {
|
||||||
|
return fmt.Sprintf("cannot remove membership %q: last active owner of the organization", e.MembershipID)
|
||||||
|
}
|
||||||
|
|
||||||
type ErrOrganizationNotFound struct{ OrganizationID gid.GID }
|
type ErrOrganizationNotFound struct{ OrganizationID gid.GID }
|
||||||
|
|
||||||
func NewOrganizationNotFoundError(organizationID gid.GID) error {
|
func NewOrganizationNotFoundError(organizationID gid.GID) error {
|
||||||
|
|||||||
@@ -257,6 +257,21 @@ func (s *OrganizationService) RemoveMember(
|
|||||||
return NewMembershipManagedBySCIMError(membershipID)
|
return NewMembershipManagedBySCIMError(membershipID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if membership.Role == coredata.MembershipRoleOwner && membership.State == coredata.MembershipStateActive {
|
||||||
|
memberships := coredata.Memberships{}
|
||||||
|
filter := coredata.NewMembershipFilter().
|
||||||
|
WithRole(coredata.MembershipRoleOwner).
|
||||||
|
WithState(coredata.MembershipStateActive)
|
||||||
|
count, err := memberships.CountByOrganizationID(ctx, tx, scope, organizationID, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count active owners: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count <= 1 {
|
||||||
|
return NewLastActiveOwnerError(membershipID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := membership.Delete(ctx, tx, scope, membershipID)
|
err := membership.Delete(ctx, tx, scope, membershipID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot delete membership: %w", err)
|
return fmt.Errorf("cannot delete membership: %w", err)
|
||||||
|
|||||||
@@ -968,10 +968,16 @@ func (r *mutationResolver) RemoveMember(ctx context.Context, input types.RemoveM
|
|||||||
err := r.iam.OrganizationService.RemoveMember(ctx, input.OrganizationID, input.MembershipID)
|
err := r.iam.OrganizationService.RemoveMember(ctx, input.OrganizationID, input.MembershipID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var errManagedBySCIM *iam.ErrMembershipManagedBySCIM
|
var errManagedBySCIM *iam.ErrMembershipManagedBySCIM
|
||||||
|
var errLastActiveOwner *iam.ErrLastActiveOwner
|
||||||
|
|
||||||
if errors.As(err, &errManagedBySCIM) {
|
if errors.As(err, &errManagedBySCIM) {
|
||||||
return nil, gqlutils.Conflict(err)
|
return nil, gqlutils.Conflict(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.As(err, &errLastActiveOwner) {
|
||||||
|
return nil, gqlutils.Conflict(err)
|
||||||
|
}
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot remove member from organization", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot remove member from organization", log.Error(err))
|
||||||
return nil, gqlutils.InternalServerError(ctx)
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user