From c184fe1f67d31ee4284069ce4989f7835378d776 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 22 Aug 2024 17:06:24 -0500 Subject: [PATCH 1/6] chore: implement generalized symmetric difference for set comparison Going to be used in Organization Sync + maybe group sync. Felt better to reuse, rather than copy --- coderd/rbac/roles.go | 26 +++---------------- coderd/util/slice/slice.go | 37 ++++++++++++++++++++++++++ coderd/util/slice/slice_test.go | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 23 deletions(-) diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 14f797ca0b4ee..db62bbd6e6d0d 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -764,29 +764,9 @@ func SiteRoles() []Role { // RBAC checks can be applied using "ActionCreate" and "ActionDelete" for // "added" and "removed" roles respectively. func ChangeRoleSet(from []RoleIdentifier, to []RoleIdentifier) (added []RoleIdentifier, removed []RoleIdentifier) { - has := make(map[RoleIdentifier]struct{}) - for _, exists := range from { - has[exists] = struct{}{} - } - - for _, roleName := range to { - // If the user already has the role assigned, we don't need to check the permission - // to reassign it. Only run permission checks on the difference in the set of - // roles. - if _, ok := has[roleName]; ok { - delete(has, roleName) - continue - } - - added = append(added, roleName) - } - - // Remaining roles are the ones removed/deleted. - for roleName := range has { - removed = append(removed, roleName) - } - - return added, removed + return slice.SymmetricDifferenceFunc(from, to, func(a, b RoleIdentifier) bool { + return a.Name == b.Name && a.OrganizationID == b.OrganizationID + }) } // Permissions is just a helper function to make building roles that list out resources diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 9bb1da930ff45..0071b40e644c6 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -107,3 +107,40 @@ func Ascending[T constraints.Ordered](a, b T) int { func Descending[T constraints.Ordered](a, b T) int { return -Ascending[T](a, b) } + +// SymmetricDifference returns the elements that need to be added and removed +// to get from set 'a' to set 'b'. +// In classical set theory notation, SymmetricDifference returns +// all elements of {add} and {remove} together. It is more useful to +// return them as their own slices. +// Example: +// +// a := []int{1, 3, 4} +// b := []int{1, 2} +// add, remove := SymmetricDifference(a, b) +// fmt.Println(add) // [2] +// fmt.Println(remove) // [3, 4] +func SymmetricDifference[T comparable](a, b []T) (add []T, remove []T) { + return Difference(b, a), Difference(a, b) +} + +// Difference returns the elements in 'a' that are not in 'b'. +func Difference[T comparable](a []T, b []T) []T { + return DifferenceFunc(a, b, func(a, b T) bool { + return a == b + }) +} + +func SymmetricDifferenceFunc[T any](a, b []T, equal func(a, b T) bool) (add []T, remove []T) { + return DifferenceFunc(a, b, equal), DifferenceFunc(b, a, equal) +} + +func DifferenceFunc[T any](a []T, b []T, equal func(a, b T) bool) []T { + tmp := make([]T, 0, len(a)) + for _, v := range a { + if !ContainsCompare(b, v, equal) { + tmp = append(tmp, v) + } + } + return tmp +} diff --git a/coderd/util/slice/slice_test.go b/coderd/util/slice/slice_test.go index ef947a13e7659..c995066983632 100644 --- a/coderd/util/slice/slice_test.go +++ b/coderd/util/slice/slice_test.go @@ -131,3 +131,49 @@ func TestOmit(t *testing.T) { slice.Omit([]string{"a", "b", "c", "d", "e", "f"}, "c", "d", "e"), ) } + +func TestSymmetricDifference(t *testing.T) { + t.Parallel() + + t.Run("Simple", func(t *testing.T) { + add, remove := slice.SymmetricDifference([]int{1, 3, 4}, []int{1, 2}) + require.ElementsMatch(t, []int{2}, add) + require.ElementsMatch(t, []int{3, 4}, remove) + }) + + t.Run("Large", func(t *testing.T) { + add, remove := slice.SymmetricDifference( + []int{1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15}, + []int{1, 3, 7, 9, 11, 13, 17}, + ) + require.ElementsMatch(t, []int{7, 9, 17}, add) + require.ElementsMatch(t, []int{2, 4, 5, 10, 12, 14, 15}, remove) + }) + + t.Run("AddOnly", func(t *testing.T) { + add, remove := slice.SymmetricDifference( + []int{1, 2}, + []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, + ) + require.ElementsMatch(t, []int{3, 4, 5, 6, 7, 8, 9}, add) + require.ElementsMatch(t, []int{}, remove) + }) + + t.Run("RemoveOnly", func(t *testing.T) { + add, remove := slice.SymmetricDifference( + []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, + []int{1, 2}, + ) + require.ElementsMatch(t, []int{}, add) + require.ElementsMatch(t, []int{3, 4, 5, 6, 7, 8, 9}, remove) + }) + + t.Run("Equal", func(t *testing.T) { + add, remove := slice.SymmetricDifference( + []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, + []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, + ) + require.ElementsMatch(t, []int{}, add) + require.ElementsMatch(t, []int{}, remove) + }) +} From 00fc33dc3aa0ad36492c9d16f36dd5493e5a099e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 22 Aug 2024 17:11:58 -0500 Subject: [PATCH 2/6] chore: linting --- coderd/util/slice/slice_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/coderd/util/slice/slice_test.go b/coderd/util/slice/slice_test.go index c995066983632..07535be3b1879 100644 --- a/coderd/util/slice/slice_test.go +++ b/coderd/util/slice/slice_test.go @@ -136,12 +136,16 @@ func TestSymmetricDifference(t *testing.T) { t.Parallel() t.Run("Simple", func(t *testing.T) { + t.Parallel() + add, remove := slice.SymmetricDifference([]int{1, 3, 4}, []int{1, 2}) require.ElementsMatch(t, []int{2}, add) require.ElementsMatch(t, []int{3, 4}, remove) }) t.Run("Large", func(t *testing.T) { + t.Parallel() + add, remove := slice.SymmetricDifference( []int{1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15}, []int{1, 3, 7, 9, 11, 13, 17}, @@ -151,6 +155,8 @@ func TestSymmetricDifference(t *testing.T) { }) t.Run("AddOnly", func(t *testing.T) { + t.Parallel() + add, remove := slice.SymmetricDifference( []int{1, 2}, []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, @@ -160,6 +166,8 @@ func TestSymmetricDifference(t *testing.T) { }) t.Run("RemoveOnly", func(t *testing.T) { + t.Parallel() + add, remove := slice.SymmetricDifference( []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{1, 2}, @@ -169,6 +177,8 @@ func TestSymmetricDifference(t *testing.T) { }) t.Run("Equal", func(t *testing.T) { + t.Parallel() + add, remove := slice.SymmetricDifference( []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, From ae75e5dcb37fc4e680708dd64c25ca2c8b0a479a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 22 Aug 2024 17:20:47 -0500 Subject: [PATCH 3/6] add tests --- coderd/util/slice/slice.go | 2 +- coderd/util/slice/slice_test.go | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 0071b40e644c6..ca3603dc28cba 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -132,7 +132,7 @@ func Difference[T comparable](a []T, b []T) []T { } func SymmetricDifferenceFunc[T any](a, b []T, equal func(a, b T) bool) (add []T, remove []T) { - return DifferenceFunc(a, b, equal), DifferenceFunc(b, a, equal) + return DifferenceFunc(b, a, equal), DifferenceFunc(a, b, equal) } func DifferenceFunc[T any](a []T, b []T, equal func(a, b T) bool) []T { diff --git a/coderd/util/slice/slice_test.go b/coderd/util/slice/slice_test.go index 07535be3b1879..5ab61f83ddbc1 100644 --- a/coderd/util/slice/slice_test.go +++ b/coderd/util/slice/slice_test.go @@ -186,4 +186,48 @@ func TestSymmetricDifference(t *testing.T) { require.ElementsMatch(t, []int{}, add) require.ElementsMatch(t, []int{}, remove) }) + + t.Run("ToEmpty", func(t *testing.T) { + t.Parallel() + + add, remove := slice.SymmetricDifference( + []int{1, 2, 3}, + []int{}, + ) + require.ElementsMatch(t, []int{}, add) + require.ElementsMatch(t, []int{1, 2, 3}, remove) + }) + + t.Run("ToNil", func(t *testing.T) { + t.Parallel() + + add, remove := slice.SymmetricDifference( + []int{1, 2, 3}, + nil, + ) + require.ElementsMatch(t, []int{}, add) + require.ElementsMatch(t, []int{1, 2, 3}, remove) + }) + + t.Run("FromEmpty", func(t *testing.T) { + t.Parallel() + + add, remove := slice.SymmetricDifference( + []int{}, + []int{1, 2, 3}, + ) + require.ElementsMatch(t, []int{1, 2, 3}, add) + require.ElementsMatch(t, []int{}, remove) + }) + + t.Run("FromNil", func(t *testing.T) { + t.Parallel() + + add, remove := slice.SymmetricDifference( + nil, + []int{1, 2, 3}, + ) + require.ElementsMatch(t, []int{1, 2, 3}, add) + require.ElementsMatch(t, []int{}, remove) + }) } From ed8972f3334f7e05da7dc7ba99e76cafaf1a93a1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 22 Aug 2024 17:24:30 -0500 Subject: [PATCH 4/6] use same function for all types --- coderd/util/slice/slice.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index ca3603dc28cba..90cefb7403eed 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -121,14 +121,8 @@ func Descending[T constraints.Ordered](a, b T) int { // fmt.Println(add) // [2] // fmt.Println(remove) // [3, 4] func SymmetricDifference[T comparable](a, b []T) (add []T, remove []T) { - return Difference(b, a), Difference(a, b) -} - -// Difference returns the elements in 'a' that are not in 'b'. -func Difference[T comparable](a []T, b []T) []T { - return DifferenceFunc(a, b, func(a, b T) bool { - return a == b - }) + f := func(a, b T) bool { return a == b } + return SymmetricDifferenceFunc(a, b, f) } func SymmetricDifferenceFunc[T any](a, b []T, equal func(a, b T) bool) (add []T, remove []T) { From cc4a46285fa9bb21041902bca3eace3481ce8bca Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 23 Aug 2024 11:27:18 -0500 Subject: [PATCH 5/6] add a go example --- coderd/util/slice/example_test.go | 20 ++++++++++++++++++++ coderd/util/slice/slice.go | 1 + 2 files changed, 21 insertions(+) create mode 100644 coderd/util/slice/example_test.go diff --git a/coderd/util/slice/example_test.go b/coderd/util/slice/example_test.go new file mode 100644 index 0000000000000..70c6caffa7033 --- /dev/null +++ b/coderd/util/slice/example_test.go @@ -0,0 +1,20 @@ +package slice_test + +import ( + "fmt" + + "github.com/coder/coder/v2/coderd/util/slice" +) + +func ExampleSymmetricDifference() { + // The goal of this function is to find the elements to add & remove from + // set 'a' to make it equal to set 'b'. + a := []int{1, 2, 5, 6} + b := []int{2, 3, 4, 5} + add, remove := slice.SymmetricDifference(a, b) + fmt.Println("Elements to add:", add) + fmt.Println("Elements to remove:", remove) + // Output: + // Elements to add: [3 4] + // Elements to remove: [1 6] +} diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 90cefb7403eed..e186e0975de70 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -113,6 +113,7 @@ func Descending[T constraints.Ordered](a, b T) int { // In classical set theory notation, SymmetricDifference returns // all elements of {add} and {remove} together. It is more useful to // return them as their own slices. +// Notation: A Δ B = (A\B) ∪ (B\A) // Example: // // a := []int{1, 3, 4} From 3cc9b0803ba2aac6f8d7ac05c404298776a0e4ac Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 23 Aug 2024 12:54:26 -0500 Subject: [PATCH 6/6] linting --- coderd/util/slice/example_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/util/slice/example_test.go b/coderd/util/slice/example_test.go index 70c6caffa7033..f17d9c4aab0ff 100644 --- a/coderd/util/slice/example_test.go +++ b/coderd/util/slice/example_test.go @@ -6,6 +6,7 @@ import ( "github.com/coder/coder/v2/coderd/util/slice" ) +//nolint:revive // They want me to error check my Printlns func ExampleSymmetricDifference() { // The goal of this function is to find the elements to add & remove from // set 'a' to make it equal to set 'b'.