Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get/add/update/remove hierarchical groups #20

Merged
merged 5 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions internal/dbtools/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,66 @@ func AuditGroupDeleted(ctx context.Context, exec boil.ContextExecutor, pID strin
return &event, event.Insert(ctx, exec, boil.Infer())
}

// AuditGroupHierarchyCreated inserts an event representing group hierarchy creation into the events table
func AuditGroupHierarchyCreated(ctx context.Context, exec boil.ContextExecutor, pID string, actor *models.User, m *models.GroupHierarchy) (*models.AuditEvent, error) {
// TODO non-user API actors don't exist in the governor database,
// we need to figure out how to handle that relationship in the audit table
var actorID null.String
if actor != nil {
actorID = null.StringFrom(actor.ID)
}

event := models.AuditEvent{
ParentID: null.StringFrom(pID),
ActorID: actorID,
SubjectGroupID: null.StringFrom(m.ParentGroupID),
Action: "group.hierarchy.added",
Changeset: calculateChangeset(&models.GroupHierarchy{}, m),
}

return &event, event.Insert(ctx, exec, boil.Infer())
}

// AuditGroupHierarchyUpdated inserts an event representing group hierarchy update into the events table
func AuditGroupHierarchyUpdated(ctx context.Context, exec boil.ContextExecutor, pID string, actor *models.User, m *models.GroupHierarchy) (*models.AuditEvent, error) {
// TODO non-user API actors don't exist in the governor database,
// we need to figure out how to handle that relationship in the audit table
var actorID null.String
if actor != nil {
actorID = null.StringFrom(actor.ID)
}

event := models.AuditEvent{
ParentID: null.StringFrom(pID),
ActorID: actorID,
SubjectGroupID: null.StringFrom(m.ParentGroupID),
Action: "group.hierarchy.updated",
Changeset: calculateChangeset(&models.GroupHierarchy{}, m),
}

return &event, event.Insert(ctx, exec, boil.Infer())
}

// AuditGroupHierarchyDeleted inserts an event representing group hierarchy deletion into the events table
func AuditGroupHierarchyDeleted(ctx context.Context, exec boil.ContextExecutor, pID string, actor *models.User, m *models.GroupHierarchy) (*models.AuditEvent, error) {
// TODO non-user API actors don't exist in the governor database,
// we need to figure out how to handle that relationship in the audit table
var actorID null.String
if actor != nil {
actorID = null.StringFrom(actor.ID)
}

event := models.AuditEvent{
ParentID: null.StringFrom(pID),
ActorID: actorID,
SubjectGroupID: null.StringFrom(m.ParentGroupID),
Action: "group.hierarchy.removed",
Changeset: []string{},
}

return &event, event.Insert(ctx, exec, boil.Infer())
}

// AuditGroupMembershipCreated inserts an event representing group membership creation into the events table
func AuditGroupMembershipCreated(ctx context.Context, exec boil.ContextExecutor, pID string, actor *models.User, m *models.GroupMembership) (*models.AuditEvent, error) {
// TODO non-user API actors don't exist in the governor database,
Expand Down
88 changes: 87 additions & 1 deletion internal/dbtools/membership_enumeration.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func GetMembersOfGroup(ctx context.Context, db *sql.DB, groupID string, shouldPo
func GetAllGroupMemberships(ctx context.Context, db *sql.DB, shouldPopulateAllModels bool) ([]EnumeratedMembership, error) {
enumeratedMemberships := []EnumeratedMembership{}

err := queries.Raw(membershipsByGroupQuery).Bind(ctx, db, &enumeratedMemberships)
err := queries.Raw(allMembershipsQuery).Bind(ctx, db, &enumeratedMemberships)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return []EnumeratedMembership{}, err
Expand All @@ -230,6 +230,82 @@ func GetAllGroupMemberships(ctx context.Context, db *sql.DB, shouldPopulateAllMo
return enumeratedMemberships, nil
}

// CheckNewHierarchyWouldCreateCycle ensures that a new hierarchy does not create a loop or cycle in the database
func CheckNewHierarchyWouldCreateCycle(ctx context.Context, db *sql.DB, parentGroupID, memberGroupID string) (bool, error) {
jacobsee marked this conversation as resolved.
Show resolved Hide resolved
hierarchies := make(map[string][]string)

hierarchyRows, err := models.GroupHierarchies().All(ctx, db)
jacobsee marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return false, err
}

for _, row := range hierarchyRows {
hierarchies[row.ParentGroupID] = append(hierarchies[row.ParentGroupID], row.MemberGroupID)
}

hierarchies[parentGroupID] = append(hierarchies[parentGroupID], memberGroupID)
tenyo marked this conversation as resolved.
Show resolved Hide resolved

var walkNode func(startingID string, hierarchies map[string][]string, visited []string) bool
walkNode = func(startingID string, hierarchies map[string][]string, visited []string) bool {
if _, exists := hierarchies[startingID]; !exists {
return false
}

if contains(visited, startingID) {
return true
}

for _, e := range hierarchies[startingID] {
if walkNode(e, hierarchies, append(visited, startingID)) {
return true
}
}

return false
}

for i := range hierarchies {
if walkNode(i, hierarchies, []string{}) {
return true, nil
}
}

return false, nil
}

// FindMemberDiff finds members present in the second EnumeratedMembership which are not present in the first
func FindMemberDiff(before, after []EnumeratedMembership) []EnumeratedMembership {
type key struct {
groupID string
userID string
}

beforeMap := make(map[key]bool)

for _, e := range before {
k := key{
groupID: e.GroupID,
userID: e.UserID,
}
beforeMap[k] = true
}

uniqueMembersAfter := make([]EnumeratedMembership, 0)

for _, e := range after {
k := key{
groupID: e.GroupID,
userID: e.UserID,
}

if _, exists := beforeMap[k]; !exists {
uniqueMembersAfter = append(uniqueMembersAfter, e)
}
}

return uniqueMembersAfter
}

func populateModels(ctx context.Context, db *sql.DB, memberships []EnumeratedMembership) ([]EnumeratedMembership, error) {
groupIDSet := make(map[string]bool)
userIDSet := make(map[string]bool)
Expand Down Expand Up @@ -315,3 +391,13 @@ func stringMapToKeySlice(m map[string]bool) []string {

return keys
}

func contains(list []string, item string) bool {
for _, i := range list {
if i == item {
return true
}
}

return false
}
25 changes: 25 additions & 0 deletions internal/dbtools/membership_enumeration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,31 @@ func TestGetMembersOfGroup(t *testing.T) {
}
}

func TestCheckNewHierarchyWouldCreateCycle(t *testing.T) {
type testCase struct {
parent string
member string
}

testCases := map[testCase]bool{
{parent: "00000002-0000-0000-0000-000000000001", member: "00000002-0000-0000-0000-000000000002"}: false,
{parent: "00000002-0000-0000-0000-000000000002", member: "00000002-0000-0000-0000-000000000003"}: false,
{parent: "00000002-0000-0000-0000-000000000003", member: "00000002-0000-0000-0000-000000000001"}: true,
{parent: "00000002-0000-0000-0000-000000000003", member: "00000002-0000-0000-0000-000000000002"}: true,
{parent: "00000002-0000-0000-0000-000000000003", member: "00000002-0000-0000-0000-000000000003"}: true,
}

for test, expect := range testCases {
t.Run(fmt.Sprintf("test for cycle: %s member of %s", test.member, test.parent), func(t *testing.T) {
result, err := CheckNewHierarchyWouldCreateCycle(context.TODO(), db, test.parent, test.member)

assert.NoError(t, err)

assert.Equal(t, expect, result)
})
}
}

// Sets this up:
//
// ┌──────┐
Expand Down
Loading