Skip to content

Commit

Permalink
add restraints to updating managed groups
Browse files Browse the repository at this point in the history
Signed-off-by: Sarah Funkhouser <[email protected]>
  • Loading branch information
golanglemonade committed Jan 2, 2025
1 parent 63decef commit 860758d
Show file tree
Hide file tree
Showing 11 changed files with 280 additions and 202 deletions.
2 changes: 1 addition & 1 deletion internal/ent/generated/group/group.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions internal/ent/generated/runtime/runtime.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions internal/ent/hooks/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ var (
ErrNoControls = errors.New("subcontrol must have at least one control assigned")
// ErrUnableToCast is returned when a type assertion fails
ErrUnableToCast = errors.New("unable to cast")
// ErrManagedGroup is returned when a user attempts to modify a managed group
ErrManagedGroup = errors.New("managed groups cannot be modified")
)

// IsUniqueConstraintError reports if the error resulted from a DB uniqueness constraint violation.
Expand Down
30 changes: 30 additions & 0 deletions internal/ent/hooks/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/theopenlane/entx"
"github.com/theopenlane/iam/auth"
"github.com/theopenlane/iam/fgax"
"github.com/theopenlane/utils/contextx"
"github.com/theopenlane/utils/gravatar"

"github.com/theopenlane/core/internal/ent/generated"
Expand Down Expand Up @@ -53,6 +54,34 @@ func HookGroup() ent.Hook {
}, ent.OpCreate|ent.OpUpdateOne)
}

// HookManagedGroups runs on group mutations to prevent updates to managed groups
func HookManagedGroups() ent.Hook {
return func(next ent.Mutator) ent.Mutator {
return hook.GroupFunc(func(ctx context.Context, m *generated.GroupMutation) (ent.Value, error) {
if m.Op().Is(ent.OpCreate) {
return next.Mutate(ctx, m)
}

groupID, ok := m.ID()
if ok && groupID != "" {
group, err := m.Client().Group.Get(ctx, groupID)
if err != nil {
log.Error().Err(err).Msg("failed to get group")

return nil, err
}

_, allowCtx := contextx.From[ManagedContextKey](ctx)
if group.IsManaged && !allowCtx {
return nil, ErrManagedGroup
}
}

return next.Mutate(ctx, m)
})
}
}

// HookGroupAuthz runs on group mutations to setup or remove relationship tuples
func HookGroupAuthz() ent.Hook {
return func(next ent.Mutator) ent.Mutator {
Expand Down Expand Up @@ -124,6 +153,7 @@ func createGroupMember(ctx context.Context, gID string, m *generated.GroupMutati
managed, _ := m.IsManaged()

role := enums.RoleAdmin

if managed {
name, _ := m.Name()
// do not add the owner to the Members group
Expand Down
7 changes: 7 additions & 0 deletions internal/ent/hooks/groupmembers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (

"entgo.io/ent"

"github.com/theopenlane/utils/contextx"

"github.com/theopenlane/core/internal/ent/generated"
"github.com/theopenlane/core/internal/ent/generated/hook"
"github.com/theopenlane/core/internal/ent/generated/orgmembership"
Expand All @@ -30,6 +32,11 @@ func HookGroupMembers() ent.Hook {
return next.Mutate(ctx, m)
}

_, allowCtx := contextx.From[ManagedContextKey](ctx)
if group.IsManaged && !allowCtx {
return nil, ErrManagedGroup
}

// ensure user is a member of the organization
exists, err := m.Client().OrgMembership.Query().
Where(orgmembership.UserID(userID)).
Expand Down
222 changes: 222 additions & 0 deletions internal/ent/hooks/managedgroups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package hooks

import (
"context"

"entgo.io/ent"
"github.com/rs/zerolog/log"
"github.com/theopenlane/entx"
"github.com/theopenlane/utils/contextx"

"github.com/theopenlane/core/internal/ent/generated"
"github.com/theopenlane/core/internal/ent/generated/group"
"github.com/theopenlane/core/internal/ent/generated/groupmembership"
"github.com/theopenlane/core/pkg/enums"
)

// ManagedContextKey is the key name managed group updates
type ManagedContextKey struct{}

const (
// AdminsGroup is the group name for all organization admins and owner, these users have full read and write access in the organization
AdminsGroup = "Admins"
// ViewersGroup is the group name for all organization members that only have view access in the organization
ViewersGroup = "Viewers"
// AllMembersGroup is the group name for all members of the organization, no matter their role
AllMembersGroup = "All Members"
)

// defaultGroups are the default groups created for an organization that are managed by the system
var defaultGroups = map[string]string{
AdminsGroup: "Openlane managed group containing all organization admins with full access",
ViewersGroup: "Openlane managed group containing all organization members with only view access",
AllMembersGroup: "Openlane managed group containing all members of the organization",
}

// generateOrganizationGroups creates the default groups for an organization that are managed by Openlane
// this includes the Admins, Viewers, and All Members groups where users are automatically added based on their role
func generateOrganizationGroups(ctx context.Context, m *generated.OrganizationMutation, orgID string) error {
// skip group creation for personal orgs
if isPersonal, _ := m.PersonalOrg(); isPersonal {
log.Debug().Msg("skipping group creation for personal org")

return nil
}

builders := make([]*generated.GroupCreate, 0, len(defaultGroups))

for name, desc := range defaultGroups {
groupInput := generated.CreateGroupInput{
Name: name,
Description: &desc,
}

builders = append(builders, m.Client().Group.Create().
SetInput(groupInput).
SetIsManaged(true).
SetOwnerID(orgID),
)
}

if err := m.Client().Group.CreateBulk(builders...).Exec(ctx); err != nil {
log.Error().Err(err).Msg("error creating system managed groups")

return err
}

log.Debug().Str("organization", orgID).Msg("created system managed groups")

return nil
}

// updateManagedGroupMembers groups adds or removes the org members to the managed system groups
func updateManagedGroupMembers(ctx context.Context, m *generated.OrgMembershipMutation) error {
// set a context key to indicate that this is a managed group update
// and allowed to skip the check for managed groups
managedCtx := contextx.With(ctx, ManagedContextKey{})

op := m.Op()

switch op {
case ent.OpCreate:
return addToManagedGroups(managedCtx, m)
case ent.OpDelete, ent.OpDeleteOne:
return removeFromManagedGroups(managedCtx, m)
case ent.OpUpdate, ent.OpUpdateOne:
if entx.CheckIsSoftDelete(managedCtx) {
return removeFromManagedGroups(managedCtx, m)
}

return updateManagedGroups(managedCtx, m)
}

return nil
}

// updateManagedGroups updates the managed groups based on the role of the user for update requests
func updateManagedGroups(ctx context.Context, m *generated.OrgMembershipMutation) error {
role, ok := m.Role()
if !ok {
role = enums.RoleMember
}

oldRole, _ := m.OldRole(ctx)

if oldRole != role {
if err := removeFromManagedGroups(ctx, m); err != nil {
return err
}
}

return addToManagedGroups(ctx, m)
}

// addToManagedGroups adds the user to the system managed groups based on their role on creation or update
func addToManagedGroups(ctx context.Context, m *generated.OrgMembershipMutation) error {
role, ok := m.Role()
if !ok {
role = enums.RoleMember
}

userID, _ := m.UserID()

switch role {
case enums.RoleMember:
if err := addMemberToManagedGroup(ctx, m, userID, ViewersGroup); err != nil {
return err
}
case enums.RoleAdmin:
if err := addMemberToManagedGroup(ctx, m, userID, AdminsGroup); err != nil {
return err
}
}

// add all users to the all users group
if m.Op() == ent.OpCreate {
return addMemberToManagedGroup(ctx, m, userID, AllMembersGroup)
}

return nil
}

// removeFromManagedGroups removes the user from the system managed groups when they are removed from the organization or their role changes
func removeFromManagedGroups(ctx context.Context, m *generated.OrgMembershipMutation) error {
role, ok := m.Role()
if !ok {
role = enums.RoleMember
}

userID, _ := m.UserID()

switch role {
case enums.RoleMember:
if err := removeMemberFromManagedGroup(ctx, m, userID, ViewersGroup); err != nil {
return err
}
case enums.RoleAdmin:
if err := removeMemberFromManagedGroup(ctx, m, userID, AdminsGroup); err != nil {
return err
}
}

// remove from the all users group if they are removed from the organization
if entx.CheckIsSoftDelete(ctx) {
return removeMemberFromManagedGroup(ctx, m, userID, AllMembersGroup)
}

return nil
}

// addMemberToManagedGroup adds the user to the system managed groups
func addMemberToManagedGroup(ctx context.Context, m *generated.OrgMembershipMutation, userID, groupName string) error {
// get the group to update
groupID, err := m.Client().Group.Query().Where(
group.IsManaged(true), // grab the managed group
group.Name(groupName),
).Only(ctx)
if err != nil {
log.Error().Err(err).Msgf("error getting managed group: %s", groupName)
return err
}

input := generated.CreateGroupMembershipInput{
Role: &enums.RoleMember,
UserID: userID,
GroupID: groupID.ID,
}

if err := m.Client().GroupMembership.Create().SetInput(input).Exec(ctx); err != nil {
log.Error().Err(err).Msg("error adding user to managed group")
return err
}

log.Debug().Str("user_id", userID).Str("group", groupName).Msg("user added to managed group")

return nil
}

func removeMemberFromManagedGroup(ctx context.Context, m *generated.OrgMembershipMutation, userID, groupName string) error {
// get the group to update
group, err := m.Client().Group.Query().Where(
group.IsManaged(true), // grab the managed group
group.Name(groupName),
).Only(ctx)
if err != nil {
log.Error().Err(err).Msgf("error getting managed group: %s", groupName)
return err
}

if _, err := m.Client().GroupMembership.Delete().Where(
groupmembership.ID(group.ID),
groupmembership.UserID(userID),
groupmembership.RoleEQ(enums.RoleMember),
).Exec(ctx); err != nil {
log.Error().Err(err).Msg("error removing user from managed group")

return err
}

log.Debug().Str("user_id", userID).Str("group", groupName).Msg("user removed from managed group")

return nil
}
49 changes: 0 additions & 49 deletions internal/ent/hooks/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,52 +463,3 @@ func updateDefaultOrgIfPersonal(ctx context.Context, userID, orgID string, clien

return nil
}

const (
AdminsGroup = "Admins"
ViewersGroup = "Viewers"
AllMembersGroup = "All Members"
)

// defaultGroups are the default groups created for an organization
var defaultGroups = map[string]string{
AdminsGroup: "Openlane managed group containing all organization admins with full access",
ViewersGroup: "Openlane managed group containing all organization members with only view access",
AllMembersGroup: "Openlane managed group containing all members of the organization",
}

// generateOrganizationGroups creates the default groups for an organization that are managed by Openlane
// this includes the Admins, Members, and All Users groups where users are automatically added based on their role
func generateOrganizationGroups(ctx context.Context, m *generated.OrganizationMutation, orgID string) error {
// skip group creation for personal orgs
if isPersonal, _ := m.PersonalOrg(); isPersonal {
log.Debug().Msg("skipping group creation for personal org")

return nil
}

builders := make([]*generated.GroupCreate, 0, len(defaultGroups))

for name, desc := range defaultGroups {
groupInput := generated.CreateGroupInput{
Name: name,
Description: &desc,
}

builders = append(builders, m.Client().Group.Create().
SetInput(groupInput).
SetIsManaged(true).
SetOwnerID(orgID),
)
}

if err := m.Client().Group.CreateBulk(builders...).Exec(ctx); err != nil {
log.Error().Err(err).Msg("error creating system managed groups")

return err
}

log.Debug().Str("organization", orgID).Msg("created system managed groups")

return nil
}
Loading

0 comments on commit 860758d

Please sign in to comment.