diff --git a/compose/crs/capabilities/capabilities.go b/compose/crs/capabilities/capabilities.go new file mode 100644 index 000000000..df1df2d3d --- /dev/null +++ b/compose/crs/capabilities/capabilities.go @@ -0,0 +1,161 @@ +package capabilities + +type ( + Capability string + Set []Capability +) + +const ( + Create Capability = "create" + Update Capability = "update" + Delete Capability = "delete" + Search Capability = "search" + Paging Capability = "paging" + Stats Capability = "stats" + Sorting Capability = "sorting" + RBAC Capability = "RBAC" +) + +var ( + full = Set{ + Create, + Update, + Search, + Paging, + Stats, + Sorting, + RBAC, + } + + accessControlCapabilities = Set{ + RBAC, + } + + createCapabilities = Set{ + RBAC, + Create, + } + + updateCapabilities = Set{ + RBAC, + Update, + } + + deleteCapabilities = Set{ + RBAC, + Update, + } + + searchCapabilities = Set{ + Search, + Paging, + Sorting, + Stats, + RBAC, + } +) + +// FullCapabilities returns all base system defined capabilities +func FullCapabilities() (cc Set) { + // Doing an union just to make a fresh copy + return full.Union(nil) +} + +// AccessControlCapabilities returns only requested capabilities used for AccessControl operations +func AccessControlCapabilities(requested ...Capability) (required Set) { + return common(accessControlCapabilities, requested) +} + +// CreateCapabilities returns only requested capabilities used for Create operations +func CreateCapabilities(requested ...Capability) (required Set) { + return common(createCapabilities, requested) +} + +// UpdateCapabilities returns only requested capabilities used for Update operations +func UpdateCapabilities(requested ...Capability) (required Set) { + return common(updateCapabilities, requested) +} + +// DeleteCapabilities returns only requested capabilities used for delete operations +func DeleteCapabilities(requested ...Capability) (required Set) { + return common(deleteCapabilities, requested) +} + +// SearchCapabilities returns only requested capabilities used for Search operations +func SearchCapabilities(requested ...Capability) (required Set) { + return common(searchCapabilities, requested) +} + +func common(aa, bb Set) Set { + return aa.Intersect(bb) +} + +// --- + +// IsSuperset is inverse IsSubset +// +// IsSuperset checks if all bb capabilities are inside aa +func (aa Set) IsSuperset(bb ...Capability) bool { + return Set(bb).IsSubset(aa...) +} + +// IsSubset checks if all aa capabilities are inside bb +func (aa Set) IsSubset(bb ...Capability) bool { + if len(aa) > len(bb) { + return false + } + + // When A is subset of B, the difference between the two must be 0 + return len(aa.Diff(bb)) == 0 +} + +// Intersect returns the intersection between the two sets +func (aa Set) Intersect(bb Set) (cc Set) { + cc = make(Set, 0, len(aa)) + for _, a := range aa { + for _, b := range bb { + if a == b { + cc = append(cc, a) + break + } + } + } + + return +} + +// Intersect returns the union between the two sets +// +// Duplicates are omitted +func (aa Set) Union(bb Set) (cc Set) { + ix := make(map[Capability]bool) + for _, c := range append(aa, bb...) { + if !ix[c] { + ix[c] = true + cc = append(cc, c) + } + } + return +} + +// Diff calculates the difference between the two capability sets +// +// The diff uses aa as base +func (aa Set) Diff(bb Set) (cc Set) { + for _, a := range aa { + found := false + for _, b := range bb { + found = a == b + if found { + break + } + } + + if found { + continue + } + cc = append(cc, a) + } + + return +} diff --git a/compose/crs/capabilities/capabilities_test.go b/compose/crs/capabilities/capabilities_test.go new file mode 100644 index 000000000..52a3c675c --- /dev/null +++ b/compose/crs/capabilities/capabilities_test.go @@ -0,0 +1,127 @@ +package capabilities + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCommonCapabilities(t *testing.T) { + cases := []struct { + name string + aa Set + bb Set + cc Set + }{{ + name: "regular", + aa: Set{ + Create, + Update, + }, + bb: Set{ + Update, + Delete, + }, + cc: Set{ + Update, + }, + }, { + name: "no commons", + aa: Set{ + Create, + }, + bb: Set{ + Delete, + }, + cc: Set{}, + }, { + name: "aa empty", + aa: nil, + bb: Set{ + Update, + Delete, + }, + cc: Set{}, + }, { + name: "bb empty", + aa: Set{ + Create, + Update, + }, + bb: nil, + cc: Set{}, + }} + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + cc := common(c.aa, c.bb) + require.Equal(t, c.cc, cc) + }) + } +} + +func TestCapabilityChecking(t *testing.T) { + cases := []struct { + name string + support Set + require Set + out bool + }{{ + name: "passing: complete match", + support: Set{ + Create, + Update, + }, + require: Set{ + Create, + Update, + }, + out: true, + }, { + name: "passing: supports more then required", + support: Set{ + Create, + Update, + Delete, + }, + require: Set{ + Create, + Update, + }, + out: true, + }, { + name: "passing: no required", + support: Set{ + Create, + Update, + Delete, + }, + require: Set{}, + out: true, + }, { + name: "passing: no required nor supported", + support: Set{}, + require: Set{}, + out: true, + }, { + name: "failing: missing support", + support: Set{ + Create, + Update, + }, + require: Set{ + Delete, + }, + out: false, + }} + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + out := c.support.IsSuperset(c.require...) + require.Equal(t, c.out, out) + + out = c.require.IsSubset(c.support...) + require.Equal(t, c.out, out) + }) + } +}