From ab61328563b943f8b31eabf3b345ffa9ac6a6cc4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 25 Mar 2022 20:21:53 -0500 Subject: [PATCH 01/42] WIP: This is a massive WIP Just testing out some ideas. The code is far from finished, and very sloppy. Committing to share it to start conversations --- coderd/authz/authztest/iterator.go | 108 ++++++++++++ coderd/authz/authztest/parser.go | 117 +++++++++++++ coderd/authz/authztest/permissions.go | 192 +++++++++++++++++++++ coderd/authz/authztest/permissions_test.go | 13 ++ coderd/authz/authztest/set.go | 81 +++++++++ coderd/authz/authztest/set_test.go | 90 ++++++++++ coderd/authz/authztest/table.go | 15 ++ 7 files changed, 616 insertions(+) create mode 100644 coderd/authz/authztest/iterator.go create mode 100644 coderd/authz/authztest/parser.go create mode 100644 coderd/authz/authztest/permissions.go create mode 100644 coderd/authz/authztest/permissions_test.go create mode 100644 coderd/authz/authztest/set.go create mode 100644 coderd/authz/authztest/set_test.go create mode 100644 coderd/authz/authztest/table.go diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/authztest/iterator.go new file mode 100644 index 0000000000000..468139ba38799 --- /dev/null +++ b/coderd/authz/authztest/iterator.go @@ -0,0 +1,108 @@ +package authztest + +type iterable interface { + Iterator() iterator +} + +type iterator interface { + iterable + + Next() bool + Permissions() Set + Reset() + ReturnSize() int + Size() int +} + +// SetIterator is very primitive, just used to hold a place in a set. +type SetIterator struct { + i int + set Set +} + +func union(sets ...Set) *SetIterator { + all := Set{} + for _, set := range sets { + all = append(all, set...) + } + return &SetIterator{ + i: 0, + set: all, + } +} + +func (si *SetIterator) Next() bool { + si.i++ + return si.i < len(si.set) +} + +func (si *SetIterator) Permissions() Set { + return Set{si.set[si.i]} +} + +func (si *SetIterator) Permission() *Permission { + return si.set[si.i] +} + +func (si *SetIterator) Reset() { + si.i = 0 +} + +func (si *SetIterator) ReturnSize() int { + return 1 +} + +func (si *SetIterator) Size() int { + return len(si.set) +} + +func (si *SetIterator) Iterator() iterator { + return si +} + +type productIterator struct { + i, j int + a Set + b Set +} + +func product(a, b Set) *productIterator { + return &productIterator{ + i: 0, + j: 0, + a: a, + b: b, + } +} + +func (s *productIterator) Next() bool { + s.i++ + if s.i >= len(s.a) { + s.i = 0 + s.j++ + } + if s.j >= len(s.b) { + return false + } + return true +} + +func (s productIterator) Permissions() Set { + return Set{s.a[s.i], s.b[s.j]} +} + +func (s *productIterator) Reset() { + s.i, s.j = 0, 0 +} + +func (s *productIterator) ReturnSize() int { + return 2 +} + +func (s *productIterator) Size() int { + return len(s.a) * len(s.b) +} + +func (s *productIterator) Iterator() iterator { + return s +} \ No newline at end of file diff --git a/coderd/authz/authztest/parser.go b/coderd/authz/authztest/parser.go new file mode 100644 index 0000000000000..3db7fbe76d3b7 --- /dev/null +++ b/coderd/authz/authztest/parser.go @@ -0,0 +1,117 @@ +package authztest + +import "fmt" + +type Parser struct { + input string + stack []interface{} + grp setGroup + + setI []iterable +} + +func ParseRole(grp setGroup, input string) *Role { + p := NewParser(grp, input) + p.parse() + return NewRole(p.setI...) +} + +func Parse(grp setGroup, input string) []iterable { + p := NewParser(grp, input) + p.parse() + return p.setI +} + +func NewParser(grp setGroup, input string) *Parser { + + return &Parser{ + grp: grp, + input: input, + stack: make([]interface{}, 0), + } +} + +func (p *Parser) skipSpace(ptr int) int { + for ptr < len(p.input) { + r := p.input[ptr] + switch r { + case ' ', '\t', '\n': + ptr++ + default: + return ptr + } + } + return ptr +} + +func (p *Parser) parse() { + ptr := 0 + for ptr < len(p.input) { + ptr = p.skipSpace(ptr) + r := p.input[ptr] + switch r { + case ' ': + ptr++ + case 'w', 's', 'o', 'm', 'u': + // Time to look ahead for the grp + ptr++ + ptr = p.handleLevel(r, ptr) + default: + panic(fmt.Errorf("cannot handle '%c' at %d", r, ptr)) + } + } +} + +func (p *Parser) handleLevel(l uint8, ptr int) int { + var lg LevelGroup + switch l { + case 'w': + lg = p.grp.Wildcard() + case 's': + lg = p.grp.Site() + case 'o': + lg = p.grp.AllOrgs() + case 'm': + lg = p.grp.OrgMem() + case 'u': + lg = p.grp.User() + } + + // time to look ahead. Find the parenthesis + var sets []Set + var start bool + var stop bool + for { + ptr = p.skipSpace(ptr) + r := p.input[ptr] + if r != '(' && !start { + panic(fmt.Sprintf("Expect a parenthesis at %d", ptr)) + } + switch r { + case '(': + start = true + case ')': + stop = true + case 'p': + sets = append(sets, lg.Positive()) + case 'n': + sets = append(sets, lg.Negative()) + case 'a': + sets = append(sets, lg.Abstain()) + case '*': + sets = append(sets, lg.All()) + case 'e': + // Add the empty perm + sets = append(sets, Set{nil}) + default: + panic(fmt.Errorf("unsupported '%c' for level set", r)) + } + ptr++ + if stop { + p.setI = append(p.setI, union(sets...)) + return ptr + } + } +} + +//func (p *Parser) diff --git a/coderd/authz/authztest/permissions.go b/coderd/authz/authztest/permissions.go new file mode 100644 index 0000000000000..c2a4dba07e6a8 --- /dev/null +++ b/coderd/authz/authztest/permissions.go @@ -0,0 +1,192 @@ +package authztest + +import "strings" + +// Permission Sets + +//type Permission [(270 / 8) + 1]byte + +// level.resource.id.action +type Permission [5]int + +func (p Permission) Type() permissionType { + return PermissionTypes[p[0]] +} + +func (p Permission) Level() level { + return Levels[p[1]] +} + +func (p Permission) ResourceType() resourceType { + return ResourceTypes[p[2]] +} + +func (p Permission) ResourceID() resourceID { + return ResourceIDs[p[3]] +} + +func (p Permission) Action() action { + return Actions[p[4]] +} + +func (p Permission) String() string { + var s strings.Builder + s.WriteString(string(p.Type())) + s.WriteString(string(p.Level())) + s.WriteRune('.') + s.WriteString(string(p.ResourceType())) + s.WriteRune('.') + s.WriteString(string(p.ResourceID())) + s.WriteRune('.') + s.WriteString(string(p.Action())) + return s.String() +} + +type permissionSet string + +const ( + SetPositive permissionSet = "j" + SetNegative permissionSet = "j!" + SetNeutral permissionSet = "a" +) + +var ( + PermissionSets = []permissionSet{SetPositive, SetNegative, SetNeutral} +) + +// TSet returns what set the permission is included in +func (p Permission) Set() permissionSet { + if p.ResourceType() == otherOption || + p.ResourceID() == otherOption || + p.Action() == otherOption { + return SetNeutral + } + if p.Type() == "+" { + return SetPositive + } + return SetNegative +} + +type permissionType string +type resourceType string +type resourceID string +type action string + +type level string + +const ( + otherOption = "other" + + levelWild level = "*" + levelSite level = "site" + levelOrg level = "org" + levelOrgMem level = "org:mem" + // levelOrgAll is a helper to get both org levels above + levelOrgAll level = "org:*" + levelUser level = "user" +) + +var ( + PermissionTypes = []permissionType{"+", "-"} + Levels = []level{levelWild, levelSite, levelOrg, levelOrgMem, levelUser} + ResourceTypes = []resourceType{"resource", "*", otherOption} + ResourceIDs = []resourceID{"rid", "*", otherOption} + Actions = []action{"action", "*", otherOption} +) + +func AllPermissions() Set { + all := make(Set, 0, 2*len(Levels)*len(ResourceTypes)*len(ResourceIDs)*len(Actions)) + for p := range PermissionTypes { + for l := range Levels { + for t := range ResourceTypes { + for i := range ResourceIDs { + for a := range Actions { + all = append(all, &Permission{p, l, t, i, a}) + } + } + } + } + } + return all +} + +// LevelGroup is all permissions for a given level +type LevelGroup map[permissionSet]Set + +func (lg LevelGroup) All() Set { + pos := lg.Positive() + neg := lg.Negative() + net := lg.Abstain() + all := make(Set, len(pos)+len(neg)+len(net)) + var i int + i += copy(all, pos) + i += copy(all, neg) + i += copy(all, net) + return all +} + +func (lg LevelGroup) Positive() Set { + return lg[SetPositive] +} + +func (lg LevelGroup) Negative() Set { + return lg[SetNegative] +} + +func (lg LevelGroup) Abstain() Set { + return lg[SetNeutral] +} + +func GroupedPermissions(perms Set) setGroup { + groups := make(setGroup) + for _, l := range append(Levels, levelOrgAll) { + groups[l] = make(LevelGroup) + } + + for _, p := range perms { + m := p.Set() + l := p.Level() + groups[l][m] = append(groups[l][m], p) + if l == levelOrg || l == levelOrgMem { + groups[levelOrgAll][m] = append(groups[levelOrgAll][m], p) + } + } + + return groups +} + +type setGroup map[level]LevelGroup + +func (s setGroup) Level(l level) LevelGroup { + return s[l] +} + +func (s setGroup) Wildcard() LevelGroup { + return s[levelWild] +} +func (s setGroup) Site() LevelGroup { + return s[levelSite] +} +func (s setGroup) Org() LevelGroup { + return s[levelOrgMem] +} +func (s setGroup) AllOrgs() LevelGroup { + return s[levelOrgAll] +} +func (s setGroup) OrgMem() LevelGroup { + return s[levelOrgMem] +} +func (s setGroup) User() LevelGroup { + return s[levelUser] +} + +type Cache struct { + setGroup +} + +func NewCache(perms Set) *Cache { + c := &Cache{ + setGroup: GroupedPermissions(perms), + } + return c +} diff --git a/coderd/authz/authztest/permissions_test.go b/coderd/authz/authztest/permissions_test.go new file mode 100644 index 0000000000000..d2bad6b7ccd73 --- /dev/null +++ b/coderd/authz/authztest/permissions_test.go @@ -0,0 +1,13 @@ +package authztest + +import ( + "testing" +) + +func TestPermissionSet(t *testing.T) { + all := AllPermissions() + c := NewCache(all) + + c.Site().Positive() + +} diff --git a/coderd/authz/authztest/set.go b/coderd/authz/authztest/set.go new file mode 100644 index 0000000000000..cfa6deb5e4dd7 --- /dev/null +++ b/coderd/authz/authztest/set.go @@ -0,0 +1,81 @@ +package authztest + +import ( + "strings" +) + +// Role can print all possible permutations of the given iterators. +type Role struct { + ReturnSize int + Size int + PermissionSets []iterator + // This is kinda werird, but the first scan should not move anything. + first bool +} + +func (r *Role) Each(ea func(set Set)) { + for r.Next() { + ea(r.Permissions()) + } +} + +func NewRole(sets ...iterable) *Role { + setInterfaces := make([]iterator, 0, len(sets)) + var retSize int + var size int = 1 + for _, s := range sets { + v := s.Iterator() + setInterfaces = append(setInterfaces, v) + retSize += v.ReturnSize() + size *= v.Size() + } + return &Role{ + ReturnSize: retSize, + Size: size, + PermissionSets: setInterfaces, + } +} + +// Next will gr +func (s *Role) Next() bool { + if !s.first { + s.first = true + return true + } + for i := range s.PermissionSets { + if s.PermissionSets[i].Next() { + break + } else { + s.PermissionSets[i].Reset() + if i == len(s.PermissionSets)-1 { + return false + } + } + } + return true +} + +func (s *Role) Permissions() Set { + all := make(Set, 0, s.Size) + for _, set := range s.PermissionSets { + all = append(all, set.Permissions()...) + } + return all +} + +type Set []*Permission + +func (s Set) String() string { + var str strings.Builder + sep := "" + for _, v := range s { + str.WriteString(sep) + str.WriteString(v.String()) + sep = ", " + } + return str.String() +} + +func (s Set) Iterator() iterator { + return union(s) +} diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go new file mode 100644 index 0000000000000..0642a23d2bf07 --- /dev/null +++ b/coderd/authz/authztest/set_test.go @@ -0,0 +1,90 @@ +package authztest + +import ( + "fmt" + "testing" +) + +func TestRole(t *testing.T) { + all := GroupedPermissions(AllPermissions()) + testCases := []struct { + Name string + Permutations *Role + Access bool + }{ + { + Name: "W+", + Permutations: ParseRole(all, "w(pa) s(*e) o(*e) u(*e)"), + Access: true, + }, + { + Name: "W-", + Permutations: ParseRole(all, "w(n) w(pae) s(*e) o(*e) u(*e)"), + Access: false, + }, + { + Name: "S+", + Permutations: ParseRole(all, "w(a) s(pa) o(*e) u(*e)"), + Access: true, + }, + { + Name: "S-", + Permutations: ParseRole(all, "w(a) s(n) s(pae) o(*e) u(*e)"), + Access: false, + }, + { + Name: "O+", + Permutations: ParseRole(all, "w(a) s(a) o(pa) u(*e)"), + Access: true, + }, + { + Name: "O-", + Permutations: ParseRole(all, "w(a) s(a) o(n) o(pae) u(*e)"), + Access: false, + }, + { + Name: "U+", + Permutations: ParseRole(all, "w(a) s(a) o(a) u(pa)"), + Access: true, + }, + { + Name: "U-", + Permutations: ParseRole(all, "w(a) s(a) o(a) u(n) u(pa)"), + Access: false, + }, + { + Name: "A0", + Permutations: ParseRole(all, "w(a) s(a) o(a) u(a)"), + Access: false, + }, + } + + var total uint64 + for _, c := range testCases { + fmt.Println(c.Name) + fmt.Printf("\tSize=%d\n", c.Permutations.Size) + total += uint64(c.Permutations.Size) + } + fmt.Printf("Total cases=%d\n", total) + + // This is how you run the test cases + //for _, c := range testCases { + // t.Run(c.Name, func(t *testing.T) { + // c.Permutations.Each(func(set Set) { + // // Actually printing all the errors would be insane + // //require.Equal(t, c.Access, FakeAuthorize(set)) + // FakeAuthorize(set) + // }) + // }) + //} +} + +func FakeAuthorize(s Set) bool { + var f bool + for _, i := range s { + if i.Type() == "+" { + f = true + } + } + return f +} diff --git a/coderd/authz/authztest/table.go b/coderd/authz/authztest/table.go new file mode 100644 index 0000000000000..d6db83561cb9c --- /dev/null +++ b/coderd/authz/authztest/table.go @@ -0,0 +1,15 @@ +package authztest + +type lookupTable struct { + Permissions []Permission +} + +func NewLookupTable(list []Permission) *lookupTable { + return &lookupTable{ + Permissions: list, + } +} + +func (l lookupTable) Permission(idx int) Permission { + return l.Permissions[idx] +} From 03e4d0f26a8bd6793993d7ae4e2465ca8e82b732 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 25 Mar 2022 20:28:34 -0500 Subject: [PATCH 02/42] More info in the print --- coderd/authz/authztest/set_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go index 0642a23d2bf07..58c731fb5acba 100644 --- a/coderd/authz/authztest/set_test.go +++ b/coderd/authz/authztest/set_test.go @@ -61,10 +61,13 @@ func TestRole(t *testing.T) { var total uint64 for _, c := range testCases { - fmt.Println(c.Name) - fmt.Printf("\tSize=%d\n", c.Permutations.Size) total += uint64(c.Permutations.Size) } + + for _, c := range testCases { + fmt.Printf("%s: Size=%10d, %10f%% of total\n", + c.Name, c.Permutations.Size, 100*(float64(c.Permutations.Size)/float64(total))) + } fmt.Printf("Total cases=%d\n", total) // This is how you run the test cases From 998129134ca9e242b6d83c1167fee4c5145d85df Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Sat, 26 Mar 2022 19:25:14 -0500 Subject: [PATCH 03/42] Fix all() --- coderd/authz/authztest/permissions.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/authz/authztest/permissions.go b/coderd/authz/authztest/permissions.go index c2a4dba07e6a8..3d0d76fbfcfe3 100644 --- a/coderd/authz/authztest/permissions.go +++ b/coderd/authz/authztest/permissions.go @@ -119,9 +119,9 @@ func (lg LevelGroup) All() Set { net := lg.Abstain() all := make(Set, len(pos)+len(neg)+len(net)) var i int - i += copy(all, pos) - i += copy(all, neg) - i += copy(all, net) + i += copy(all[i:], pos) + i += copy(all[i:], neg) + i += copy(all[i:], net) return all } From 3ab32da3f54a25a76fc40b8e14aa411b05e8ad4f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Sat, 26 Mar 2022 19:25:32 -0500 Subject: [PATCH 04/42] reduce the amount of memoery allocated --- coderd/authz/authztest/set.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/coderd/authz/authztest/set.go b/coderd/authz/authztest/set.go index cfa6deb5e4dd7..2baddee05e978 100644 --- a/coderd/authz/authztest/set.go +++ b/coderd/authz/authztest/set.go @@ -11,6 +11,8 @@ type Role struct { PermissionSets []iterator // This is kinda werird, but the first scan should not move anything. first bool + + buffer []*Permission } func (r *Role) Each(ea func(set Set)) { @@ -33,6 +35,7 @@ func NewRole(sets ...iterable) *Role { ReturnSize: retSize, Size: size, PermissionSets: setInterfaces, + buffer: make([]*Permission, retSize), } } @@ -56,6 +59,10 @@ func (s *Role) Next() bool { } func (s *Role) Permissions() Set { + var i int + for _, set := range s.PermissionSets { + i += copy(s.buffer[i:], set.Permissions()) + } all := make(Set, 0, s.Size) for _, set := range s.PermissionSets { all = append(all, set.Permissions()...) From e1d589317c692a4d35fae65e0708e30d612592a9 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 28 Mar 2022 12:49:18 -0500 Subject: [PATCH 05/42] Reuse a buffer --- coderd/authz/authztest/iterator.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/authztest/iterator.go index 468139ba38799..bf07bd6ab64de 100644 --- a/coderd/authz/authztest/iterator.go +++ b/coderd/authz/authztest/iterator.go @@ -16,8 +16,9 @@ type iterator interface { // SetIterator is very primitive, just used to hold a place in a set. type SetIterator struct { - i int - set Set + i int + set Set + buffer Set } func union(sets ...Set) *SetIterator { @@ -26,8 +27,9 @@ func union(sets ...Set) *SetIterator { all = append(all, set...) } return &SetIterator{ - i: 0, - set: all, + i: 0, + set: all, + buffer: make(Set, 1), } } @@ -37,7 +39,8 @@ func (si *SetIterator) Next() bool { } func (si *SetIterator) Permissions() Set { - return Set{si.set[si.i]} + si.buffer[0] = si.set[si.i] + return si.buffer } func (si *SetIterator) Permission() *Permission { @@ -61,18 +64,21 @@ func (si *SetIterator) Iterator() iterator { } type productIterator struct { - i, j int - a Set - b Set + i, j int + a Set + b Set + buffer Set } func product(a, b Set) *productIterator { - return &productIterator{ + i := &productIterator{ i: 0, j: 0, a: a, b: b, } + i.buffer = make(Set, i.ReturnSize()) + return i } func (s *productIterator) Next() bool { @@ -88,7 +94,9 @@ func (s *productIterator) Next() bool { } func (s productIterator) Permissions() Set { - return Set{s.a[s.i], s.b[s.j]} + s.buffer[0] = s.a[s.i] + s.buffer[1] = s.b[s.j] + return s.buffer } func (s *productIterator) Reset() { @@ -105,4 +113,4 @@ func (s *productIterator) Size() int { func (s *productIterator) Iterator() iterator { return s -} \ No newline at end of file +} From 84a90f3cc2c256ee900ab0ba8f7536fdb896741d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 29 Mar 2022 09:39:45 -0500 Subject: [PATCH 06/42] fix: use return size over size --- coderd/authz/authztest/set.go | 8 ++++- coderd/authz/authztest/table.go | 15 ---------- coderd/authz/permission.go | 0 coderd/authz/permission_test.go | 44 ++++++++++++++++++++++++++++ coderd/authz/testdata/group.go | 0 coderd/authz/testdata/permissions.go | 0 coderd/authz/testdata/role.go | 0 coderd/authz/testdata/set.go | 0 8 files changed, 51 insertions(+), 16 deletions(-) delete mode 100644 coderd/authz/authztest/table.go create mode 100644 coderd/authz/permission.go create mode 100644 coderd/authz/permission_test.go create mode 100644 coderd/authz/testdata/group.go create mode 100644 coderd/authz/testdata/permissions.go create mode 100644 coderd/authz/testdata/role.go create mode 100644 coderd/authz/testdata/set.go diff --git a/coderd/authz/authztest/set.go b/coderd/authz/authztest/set.go index 2baddee05e978..287500a4a8e16 100644 --- a/coderd/authz/authztest/set.go +++ b/coderd/authz/authztest/set.go @@ -39,6 +39,12 @@ func NewRole(sets ...iterable) *Role { } } +func (s *Role) Reset() { + for _, i := range s.PermissionSets { + i.Reset() + } +} + // Next will gr func (s *Role) Next() bool { if !s.first { @@ -63,7 +69,7 @@ func (s *Role) Permissions() Set { for _, set := range s.PermissionSets { i += copy(s.buffer[i:], set.Permissions()) } - all := make(Set, 0, s.Size) + all := make(Set, 0, s.ReturnSize) for _, set := range s.PermissionSets { all = append(all, set.Permissions()...) } diff --git a/coderd/authz/authztest/table.go b/coderd/authz/authztest/table.go deleted file mode 100644 index d6db83561cb9c..0000000000000 --- a/coderd/authz/authztest/table.go +++ /dev/null @@ -1,15 +0,0 @@ -package authztest - -type lookupTable struct { - Permissions []Permission -} - -func NewLookupTable(list []Permission) *lookupTable { - return &lookupTable{ - Permissions: list, - } -} - -func (l lookupTable) Permission(idx int) Permission { - return l.Permissions[idx] -} diff --git a/coderd/authz/permission.go b/coderd/authz/permission.go new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go new file mode 100644 index 0000000000000..c30f69bbb91aa --- /dev/null +++ b/coderd/authz/permission_test.go @@ -0,0 +1,44 @@ +package authz + +import ( + crand "github.com/coder/coder/cryptorand" + "testing" +) + +func BenchmarkPermissionString(b *testing.B) { + total := 10000 + if b.N < total { + total = b.N + } + perms := make([]Permission, b.N) + for n := 0; n < total; n++ { + perms[n] = RandomPermission() + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + var _ = perms[n%total].String() + } +} + +var resourceTypes = []string{ + "project", "config", "user", "user_role", + "workspace", "dev-url", "metric", "*", +} + +var actions = []string{ + "read", "create", "delete", "modify", "*", +} + +func RandomPermission() Permission { + n, _ := crand.Intn(len(PermissionLevels)) + m, _ := crand.Intn(len(resourceTypes)) + a, _ := crand.Intn(len(actions)) + return Permission{ + Sign: n%2 == 0, + Level: PermissionLevels[n], + ResourceType: resourceTypes[m], + ResourceID: "*", + Action: actions[a], + } +} diff --git a/coderd/authz/testdata/group.go b/coderd/authz/testdata/group.go new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/coderd/authz/testdata/permissions.go b/coderd/authz/testdata/permissions.go new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/coderd/authz/testdata/role.go b/coderd/authz/testdata/role.go new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/coderd/authz/testdata/set.go b/coderd/authz/testdata/set.go new file mode 100644 index 0000000000000..e69de29bb2d1d From 1fac0d9c1eb6f4e44452eeec97b7f622bde49f3a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 29 Mar 2022 09:41:34 -0500 Subject: [PATCH 07/42] WIP: don't look at this --- coderd/authz/authztest/set_test.go | 36 +++++++++++++------ coderd/authz/permission.go | 53 ++++++++++++++++++++++++++++ coderd/authz/testdata/group.go | 13 +++++++ coderd/authz/testdata/permissions.go | 53 ++++++++++++++++++++++++++++ coderd/authz/testdata/role.go | 10 ++++++ coderd/authz/testdata/set.go | 20 +++++++++++ 6 files changed, 175 insertions(+), 10 deletions(-) diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go index 58c731fb5acba..1a992fad0e743 100644 --- a/coderd/authz/authztest/set_test.go +++ b/coderd/authz/authztest/set_test.go @@ -5,14 +5,27 @@ import ( "testing" ) +func BenchmarkRole(b *testing.B) { + all := GroupedPermissions(AllPermissions()) + r := ParseRole(all, "w(pa) s(*e) s(*e) s(*e) s(pe) s(pe) s(*) s(*)") + b.ResetTimer() + for n := 0; n < b.N; n++ { + if !r.Next() { + r.Reset() + } + FakeAuthorize(r.Permissions()) + } +} + func TestRole(t *testing.T) { all := GroupedPermissions(AllPermissions()) testCases := []struct { Name string Permutations *Role Access bool - }{ + }{ // 410,367,658 { + // [w] x [s1, s2, ""] = (w, s1), (w, s2), (w, "") Name: "W+", Permutations: ParseRole(all, "w(pa) s(*e) o(*e) u(*e)"), Access: true, @@ -71,20 +84,23 @@ func TestRole(t *testing.T) { fmt.Printf("Total cases=%d\n", total) // This is how you run the test cases - //for _, c := range testCases { - // t.Run(c.Name, func(t *testing.T) { - // c.Permutations.Each(func(set Set) { - // // Actually printing all the errors would be insane - // //require.Equal(t, c.Access, FakeAuthorize(set)) - // FakeAuthorize(set) - // }) - // }) - //} + for _, c := range testCases { + //t.Run(c.Name, func(t *testing.T) { + c.Permutations.Each(func(set Set) { + // Actually printing all the errors would be insane + //require.Equal(t, c.Access, FakeAuthorize(set)) + FakeAuthorize(set) + }) + //}) + } } func FakeAuthorize(s Set) bool { var f bool for _, i := range s { + if i == nil { + continue + } if i.Type() == "+" { f = true } diff --git a/coderd/authz/permission.go b/coderd/authz/permission.go index e69de29bb2d1d..8db492ec92463 100644 --- a/coderd/authz/permission.go +++ b/coderd/authz/permission.go @@ -0,0 +1,53 @@ +package authz + +import "strings" + +type permLevel string + +const ( + LevelWildcard permLevel = "*" + LevelSite permLevel = "site" + LevelOrg permLevel = "org" + LevelUser permLevel = "user" +) + +var PermissionLevels = [4]permLevel{LevelWildcard, LevelSite, LevelOrg, LevelUser} + +type Permission struct { + // Sign is positive or negative. + // True = Positive, False = negative + Sign bool + Level permLevel + // LevelID is used for identifying a particular org. + // org:1234 + LevelID string + + ResourceType string + ResourceID string + Action string +} + +// String returns the ... string formatted permission. +// A string builder is used to be the most efficient. +func (p Permission) String() string { + var s strings.Builder + // This could be 1 more than the actual capacity. But being 1 byte over for capacity is ok. + s.Grow(1 + 4 + len(p.Level) + len(p.LevelID) + len(p.ResourceType) + len(p.ResourceID) + len(p.Action)) + if p.Sign { + s.WriteRune('+') + } else { + s.WriteRune('-') + } + s.WriteString(string(p.Level)) + if p.LevelID != "" { + s.WriteRune(':') + s.WriteString(p.LevelID) + } + s.WriteRune('.') + s.WriteString(p.ResourceType) + s.WriteRune('.') + s.WriteString(p.ResourceID) + s.WriteRune('.') + s.WriteString(p.Action) + return s.String() +} diff --git a/coderd/authz/testdata/group.go b/coderd/authz/testdata/group.go index e69de29bb2d1d..860b8eb9cf634 100644 --- a/coderd/authz/testdata/group.go +++ b/coderd/authz/testdata/group.go @@ -0,0 +1,13 @@ +package testdata + +type permissionSet string + +const ( + SetPositive permissionSet = "j" + SetNegative permissionSet = "j!" + SetNeutral permissionSet = "a" +) + +var ( + PermissionSets = []permissionSet{SetPositive, SetNegative, SetNeutral} +) diff --git a/coderd/authz/testdata/permissions.go b/coderd/authz/testdata/permissions.go index e69de29bb2d1d..e8ab91294e6e6 100644 --- a/coderd/authz/testdata/permissions.go +++ b/coderd/authz/testdata/permissions.go @@ -0,0 +1,53 @@ +package testdata + +import ( + . "github.com/coder/coder/coderd/authz" +) + +type level string + +const ( + otherOption = "other" + + levelWild level = "*" + levelSite level = "site" + levelOrg level = "org" + levelOrgMem level = "org:mem" + // levelOrgAll is a helper to get both org levels above + levelOrgAll level = "org:*" + levelUser level = "user" +) + +var ( + PermissionTypes = []bool{true, false} + Levels = PermissionLevels + LevelIDs = []string{"", "mem"} + ResourceTypes = []string{"resource", "*", otherOption} + ResourceIDs = []string{"rid", "*", otherOption} + Actions = []string{"action", "*", otherOption} +) + +func AllPermissions() Set { + all := make(Set, 0, 2*len(Levels)*len(LevelIDs)*len(ResourceTypes)*len(ResourceIDs)*len(Actions)) + for _, p := range PermissionTypes { + for _, l := range Levels { + for _, lid := range LevelIDs { + for _, t := range ResourceTypes { + for _, i := range ResourceIDs { + for _, a := range Actions { + all = append(all, &Permission{ + Sign: p, + Level: l, + LevelID: lid, + ResourceType: t, + ResourceID: i, + Action: a, + }) + } + } + } + } + } + } + return all +} diff --git a/coderd/authz/testdata/role.go b/coderd/authz/testdata/role.go index e69de29bb2d1d..121086229ad68 100644 --- a/coderd/authz/testdata/role.go +++ b/coderd/authz/testdata/role.go @@ -0,0 +1,10 @@ +package testdata + +import ( + . "github.com/coder/coder/coderd/authz" +) + +var _ Permission + +type Role struct { +} \ No newline at end of file diff --git a/coderd/authz/testdata/set.go b/coderd/authz/testdata/set.go index e69de29bb2d1d..9ddea4c4a3df1 100644 --- a/coderd/authz/testdata/set.go +++ b/coderd/authz/testdata/set.go @@ -0,0 +1,20 @@ +package testdata + +import ( + "strings" + + . "github.com/coder/coder/coderd/authz" +) + +type Set []*Permission + +func (s Set) String() string { + var str strings.Builder + sep := "" + for _, v := range s { + str.WriteString(sep) + str.WriteString(v.String()) + sep = ", " + } + return str.String() +} From 1e3aac05673922185e42e8b678a1aaa0bfa86093 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 30 Mar 2022 15:40:42 +0100 Subject: [PATCH 08/42] =?UTF-8?q?WIP:=20=20=F0=9F=8D=90=20auth->=20testdat?= =?UTF-8?q?a,=20refactoring=20and=20restructuring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coderd/authz/authztest/permissions.go | 192 ------------------ coderd/authz/authztest/permissions_test.go | 13 -- coderd/authz/authztest/set.go | 94 --------- coderd/authz/testdata/group.go | 30 ++- coderd/authz/testdata/group_test.go | 31 +++ .../authz/{authztest => testdata}/iterator.go | 6 +- coderd/authz/testdata/level.go | 98 +++++++++ .../authz/{authztest => testdata}/parser.go | 14 +- coderd/authz/testdata/permissions.go | 60 +++--- coderd/authz/testdata/role.go | 75 ++++++- coderd/authz/testdata/set.go | 6 + .../authz/{authztest => testdata}/set_test.go | 32 +-- 12 files changed, 299 insertions(+), 352 deletions(-) delete mode 100644 coderd/authz/authztest/permissions.go delete mode 100644 coderd/authz/authztest/permissions_test.go delete mode 100644 coderd/authz/authztest/set.go create mode 100644 coderd/authz/testdata/group_test.go rename coderd/authz/{authztest => testdata}/iterator.go (96%) create mode 100644 coderd/authz/testdata/level.go rename coderd/authz/{authztest => testdata}/parser.go (89%) rename coderd/authz/{authztest => testdata}/set_test.go (76%) diff --git a/coderd/authz/authztest/permissions.go b/coderd/authz/authztest/permissions.go deleted file mode 100644 index 3d0d76fbfcfe3..0000000000000 --- a/coderd/authz/authztest/permissions.go +++ /dev/null @@ -1,192 +0,0 @@ -package authztest - -import "strings" - -// Permission Sets - -//type Permission [(270 / 8) + 1]byte - -// level.resource.id.action -type Permission [5]int - -func (p Permission) Type() permissionType { - return PermissionTypes[p[0]] -} - -func (p Permission) Level() level { - return Levels[p[1]] -} - -func (p Permission) ResourceType() resourceType { - return ResourceTypes[p[2]] -} - -func (p Permission) ResourceID() resourceID { - return ResourceIDs[p[3]] -} - -func (p Permission) Action() action { - return Actions[p[4]] -} - -func (p Permission) String() string { - var s strings.Builder - s.WriteString(string(p.Type())) - s.WriteString(string(p.Level())) - s.WriteRune('.') - s.WriteString(string(p.ResourceType())) - s.WriteRune('.') - s.WriteString(string(p.ResourceID())) - s.WriteRune('.') - s.WriteString(string(p.Action())) - return s.String() -} - -type permissionSet string - -const ( - SetPositive permissionSet = "j" - SetNegative permissionSet = "j!" - SetNeutral permissionSet = "a" -) - -var ( - PermissionSets = []permissionSet{SetPositive, SetNegative, SetNeutral} -) - -// TSet returns what set the permission is included in -func (p Permission) Set() permissionSet { - if p.ResourceType() == otherOption || - p.ResourceID() == otherOption || - p.Action() == otherOption { - return SetNeutral - } - if p.Type() == "+" { - return SetPositive - } - return SetNegative -} - -type permissionType string -type resourceType string -type resourceID string -type action string - -type level string - -const ( - otherOption = "other" - - levelWild level = "*" - levelSite level = "site" - levelOrg level = "org" - levelOrgMem level = "org:mem" - // levelOrgAll is a helper to get both org levels above - levelOrgAll level = "org:*" - levelUser level = "user" -) - -var ( - PermissionTypes = []permissionType{"+", "-"} - Levels = []level{levelWild, levelSite, levelOrg, levelOrgMem, levelUser} - ResourceTypes = []resourceType{"resource", "*", otherOption} - ResourceIDs = []resourceID{"rid", "*", otherOption} - Actions = []action{"action", "*", otherOption} -) - -func AllPermissions() Set { - all := make(Set, 0, 2*len(Levels)*len(ResourceTypes)*len(ResourceIDs)*len(Actions)) - for p := range PermissionTypes { - for l := range Levels { - for t := range ResourceTypes { - for i := range ResourceIDs { - for a := range Actions { - all = append(all, &Permission{p, l, t, i, a}) - } - } - } - } - } - return all -} - -// LevelGroup is all permissions for a given level -type LevelGroup map[permissionSet]Set - -func (lg LevelGroup) All() Set { - pos := lg.Positive() - neg := lg.Negative() - net := lg.Abstain() - all := make(Set, len(pos)+len(neg)+len(net)) - var i int - i += copy(all[i:], pos) - i += copy(all[i:], neg) - i += copy(all[i:], net) - return all -} - -func (lg LevelGroup) Positive() Set { - return lg[SetPositive] -} - -func (lg LevelGroup) Negative() Set { - return lg[SetNegative] -} - -func (lg LevelGroup) Abstain() Set { - return lg[SetNeutral] -} - -func GroupedPermissions(perms Set) setGroup { - groups := make(setGroup) - for _, l := range append(Levels, levelOrgAll) { - groups[l] = make(LevelGroup) - } - - for _, p := range perms { - m := p.Set() - l := p.Level() - groups[l][m] = append(groups[l][m], p) - if l == levelOrg || l == levelOrgMem { - groups[levelOrgAll][m] = append(groups[levelOrgAll][m], p) - } - } - - return groups -} - -type setGroup map[level]LevelGroup - -func (s setGroup) Level(l level) LevelGroup { - return s[l] -} - -func (s setGroup) Wildcard() LevelGroup { - return s[levelWild] -} -func (s setGroup) Site() LevelGroup { - return s[levelSite] -} -func (s setGroup) Org() LevelGroup { - return s[levelOrgMem] -} -func (s setGroup) AllOrgs() LevelGroup { - return s[levelOrgAll] -} -func (s setGroup) OrgMem() LevelGroup { - return s[levelOrgMem] -} -func (s setGroup) User() LevelGroup { - return s[levelUser] -} - -type Cache struct { - setGroup -} - -func NewCache(perms Set) *Cache { - c := &Cache{ - setGroup: GroupedPermissions(perms), - } - return c -} diff --git a/coderd/authz/authztest/permissions_test.go b/coderd/authz/authztest/permissions_test.go deleted file mode 100644 index d2bad6b7ccd73..0000000000000 --- a/coderd/authz/authztest/permissions_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package authztest - -import ( - "testing" -) - -func TestPermissionSet(t *testing.T) { - all := AllPermissions() - c := NewCache(all) - - c.Site().Positive() - -} diff --git a/coderd/authz/authztest/set.go b/coderd/authz/authztest/set.go deleted file mode 100644 index 287500a4a8e16..0000000000000 --- a/coderd/authz/authztest/set.go +++ /dev/null @@ -1,94 +0,0 @@ -package authztest - -import ( - "strings" -) - -// Role can print all possible permutations of the given iterators. -type Role struct { - ReturnSize int - Size int - PermissionSets []iterator - // This is kinda werird, but the first scan should not move anything. - first bool - - buffer []*Permission -} - -func (r *Role) Each(ea func(set Set)) { - for r.Next() { - ea(r.Permissions()) - } -} - -func NewRole(sets ...iterable) *Role { - setInterfaces := make([]iterator, 0, len(sets)) - var retSize int - var size int = 1 - for _, s := range sets { - v := s.Iterator() - setInterfaces = append(setInterfaces, v) - retSize += v.ReturnSize() - size *= v.Size() - } - return &Role{ - ReturnSize: retSize, - Size: size, - PermissionSets: setInterfaces, - buffer: make([]*Permission, retSize), - } -} - -func (s *Role) Reset() { - for _, i := range s.PermissionSets { - i.Reset() - } -} - -// Next will gr -func (s *Role) Next() bool { - if !s.first { - s.first = true - return true - } - for i := range s.PermissionSets { - if s.PermissionSets[i].Next() { - break - } else { - s.PermissionSets[i].Reset() - if i == len(s.PermissionSets)-1 { - return false - } - } - } - return true -} - -func (s *Role) Permissions() Set { - var i int - for _, set := range s.PermissionSets { - i += copy(s.buffer[i:], set.Permissions()) - } - all := make(Set, 0, s.ReturnSize) - for _, set := range s.PermissionSets { - all = append(all, set.Permissions()...) - } - return all -} - -type Set []*Permission - -func (s Set) String() string { - var str strings.Builder - sep := "" - for _, v := range s { - str.WriteString(sep) - str.WriteString(v.String()) - sep = ", " - } - return str.String() -} - -func (s Set) Iterator() iterator { - return union(s) -} diff --git a/coderd/authz/testdata/group.go b/coderd/authz/testdata/group.go index 860b8eb9cf634..b7fc37c786448 100644 --- a/coderd/authz/testdata/group.go +++ b/coderd/authz/testdata/group.go @@ -1,13 +1,33 @@ package testdata -type permissionSet string +type PermissionSet string const ( - SetPositive permissionSet = "j" - SetNegative permissionSet = "j!" - SetNeutral permissionSet = "a" + SetPositive PermissionSet = "j" + SetNegative PermissionSet = "j!" + SetNeutral PermissionSet = "a" ) var ( - PermissionSets = []permissionSet{SetPositive, SetNegative, SetNeutral} + PermissionSets = []PermissionSet{SetPositive, SetNegative, SetNeutral} ) + +var nilSet = Set{nil} + +// *.*.*.* +//var PermissionSetWPlus = NewRole( +// all.Wildcard().Positive(), +// union(all.Wildcard().Abstain(), nilSet), +// +// union(all.Site().Positive(), nilSet), +// union(all.Site().Negative(), nilSet), +// union(all.Site().Abstain(), nilSet), +// +// union(all.AllOrgs().Positive(), nilSet), +// union(all.AllOrgs().Negative(), nilSet), +// union(all.AllOrgs().Abstain(), nilSet), +// +// union(all.User().Positive(), nilSet), +// union(all.User().Negative(), nilSet), +// union(all.User().Abstain(), nilSet), +//) diff --git a/coderd/authz/testdata/group_test.go b/coderd/authz/testdata/group_test.go new file mode 100644 index 0000000000000..f84e132e72fab --- /dev/null +++ b/coderd/authz/testdata/group_test.go @@ -0,0 +1,31 @@ +package testdata + +import ( + "fmt" + "testing" +) + +func Test_PermissionSetWPlusSearchSpace(t *testing.T) { + all := GroupedPermissions(AllPermissions()) + wplus := NewRole( + all.Wildcard().Positive(), + union(all.Wildcard().Abstain()[:1], nilSet), + + union(all.Site().Positive()[:1], nilSet), + union(all.Site().Negative()[:1], nilSet), + union(all.Site().Abstain()[:1], nilSet), + + union(all.AllOrgs().Positive()[:1], nilSet), + union(all.AllOrgs().Negative()[:1], nilSet), + union(all.AllOrgs().Abstain()[:1], nilSet), + + union(all.User().Positive()[:1], nilSet), + union(all.User().Negative()[:1], nilSet), + union(all.User().Abstain()[:1], nilSet), + ) + fmt.Println(wplus.N) + fmt.Println(len(AllPermissions())) + for k, v := range all { + fmt.Printf("%s=%d\n", string(k), len(v.All())) + } +} diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/testdata/iterator.go similarity index 96% rename from coderd/authz/authztest/iterator.go rename to coderd/authz/testdata/iterator.go index bf07bd6ab64de..31cc3920b8bde 100644 --- a/coderd/authz/authztest/iterator.go +++ b/coderd/authz/testdata/iterator.go @@ -1,4 +1,8 @@ -package authztest +package testdata + +import ( + . "github.com/coder/coder/coderd/authz" +) type iterable interface { Iterator() iterator diff --git a/coderd/authz/testdata/level.go b/coderd/authz/testdata/level.go new file mode 100644 index 0000000000000..393dfa85e8d5a --- /dev/null +++ b/coderd/authz/testdata/level.go @@ -0,0 +1,98 @@ +package testdata + +import "github.com/coder/coder/coderd/authz" + +type level string + +const ( + levelWild level = "level-wild" + levelSite level = "level-site" + levelOrg level = "level-org" + levelOrgMem level = "level-org:mem" + // levelOrgAll is a helper to get both org levels above + levelOrgAll level = "level-org:*" + levelUser level = "level-user" +) + +// LevelGroup is all permissions for a given level +type LevelGroup map[PermissionSet]Set + +func (lg LevelGroup) All() Set { + pos := lg.Positive() + neg := lg.Negative() + net := lg.Abstain() + all := make(Set, len(pos)+len(neg)+len(net)) + var i int + i += copy(all[i:], pos) + i += copy(all[i:], neg) + i += copy(all[i:], net) + return all +} + +func (lg LevelGroup) Positive() Set { + return lg[SetPositive] +} + +func (lg LevelGroup) Negative() Set { + return lg[SetNegative] +} + +func (lg LevelGroup) Abstain() Set { + return lg[SetNeutral] +} + +func GroupedPermissions(perms Set) SetGroup { + groups := make(SetGroup) + allLevelKeys := []level{levelWild, levelSite, levelOrg, levelOrgMem, levelOrgAll, levelUser} + + for _, l := range allLevelKeys { + groups[l] = make(LevelGroup) + } + + for _, p := range perms { + m := Impact(p) + switch { + case p.Level == authz.LevelSite: + groups[levelSite][m] = append(groups[levelSite][m], p) + case p.Level == authz.LevelOrg: + groups[levelOrgAll][m] = append(groups[levelOrgAll][m], p) + if p.LevelID == "" || p.LevelID == "*" { + groups[levelOrg][m] = append(groups[levelOrg][m], p) + } else { + groups[levelOrgMem][m] = append(groups[levelOrgMem][m], p) + } + case p.Level == authz.LevelUser: + groups[levelUser][m] = append(groups[levelUser][m], p) + case p.Level == authz.LevelWildcard: + groups[levelWild][m] = append(groups[levelWild][m], p) + } + } + + return groups +} + +type SetGroup map[level]LevelGroup + +func (s SetGroup) Wildcard() LevelGroup { + return s[levelWild] +} + +func (s SetGroup) Site() LevelGroup { + return s[levelSite] +} + +func (s SetGroup) Org() LevelGroup { + return s[levelOrg] +} + +func (s SetGroup) AllOrgs() LevelGroup { + return s[levelOrgAll] +} + +func (s SetGroup) OrgMem() LevelGroup { + return s[levelOrgMem] +} + +func (s SetGroup) User() LevelGroup { + return s[levelUser] +} diff --git a/coderd/authz/authztest/parser.go b/coderd/authz/testdata/parser.go similarity index 89% rename from coderd/authz/authztest/parser.go rename to coderd/authz/testdata/parser.go index 3db7fbe76d3b7..9ab28a29af334 100644 --- a/coderd/authz/authztest/parser.go +++ b/coderd/authz/testdata/parser.go @@ -1,28 +1,30 @@ -package authztest +package testdata -import "fmt" +import ( + "fmt" +) type Parser struct { input string stack []interface{} - grp setGroup + grp SetGroup setI []iterable } -func ParseRole(grp setGroup, input string) *Role { +func ParseRole(grp SetGroup, input string) *Role { p := NewParser(grp, input) p.parse() return NewRole(p.setI...) } -func Parse(grp setGroup, input string) []iterable { +func Parse(grp SetGroup, input string) []iterable { p := NewParser(grp, input) p.parse() return p.setI } -func NewParser(grp setGroup, input string) *Parser { +func NewParser(grp SetGroup, input string) *Parser { return &Parser{ grp: grp, diff --git a/coderd/authz/testdata/permissions.go b/coderd/authz/testdata/permissions.go index e8ab91294e6e6..8a889d820be2b 100644 --- a/coderd/authz/testdata/permissions.go +++ b/coderd/authz/testdata/permissions.go @@ -4,46 +4,45 @@ import ( . "github.com/coder/coder/coderd/authz" ) -type level string - const ( otherOption = "other" - - levelWild level = "*" - levelSite level = "site" - levelOrg level = "org" - levelOrgMem level = "org:mem" - // levelOrgAll is a helper to get both org levels above - levelOrgAll level = "org:*" - levelUser level = "user" ) var ( - PermissionTypes = []bool{true, false} - Levels = PermissionLevels - LevelIDs = []string{"", "mem"} - ResourceTypes = []string{"resource", "*", otherOption} - ResourceIDs = []string{"rid", "*", otherOption} - Actions = []string{"action", "*", otherOption} + Levels = PermissionLevels + LevelIDs = []string{"", "mem"} + ResourceTypes = []string{"resource", "*", otherOption} + ResourceIDs = []string{"rid", "*", otherOption} + Actions = []string{"action", "*", otherOption} ) +// AllPermissions returns all the possible permissions ever. func AllPermissions() Set { - all := make(Set, 0, 2*len(Levels)*len(LevelIDs)*len(ResourceTypes)*len(ResourceIDs)*len(Actions)) - for _, p := range PermissionTypes { + permissionTypes := []bool{true, false} + all := make(Set, 0, len(permissionTypes)*len(Levels)*len(LevelIDs)*len(ResourceTypes)*len(ResourceIDs)*len(Actions)) + for _, s := range permissionTypes { for _, l := range Levels { - for _, lid := range LevelIDs { - for _, t := range ResourceTypes { - for _, i := range ResourceIDs { - for _, a := range Actions { + for _, t := range ResourceTypes { + for _, i := range ResourceIDs { + for _, a := range Actions { + if l == LevelOrg { all = append(all, &Permission{ - Sign: p, + Sign: s, Level: l, - LevelID: lid, + LevelID: "mem", ResourceType: t, ResourceID: i, Action: a, }) } + all = append(all, &Permission{ + Sign: s, + Level: l, + LevelID: "", + ResourceType: t, + ResourceID: i, + Action: a, + }) } } } @@ -51,3 +50,16 @@ func AllPermissions() Set { } return all } + +// Impact returns the impact (positive, negative, abstain) of p +func Impact(p *Permission) PermissionSet { + if p.ResourceType == otherOption || + p.ResourceID == otherOption || + p.Action == otherOption { + return SetNeutral + } + if p.Sign { + return SetPositive + } + return SetNegative +} diff --git a/coderd/authz/testdata/role.go b/coderd/authz/testdata/role.go index 121086229ad68..30c70730e5efa 100644 --- a/coderd/authz/testdata/role.go +++ b/coderd/authz/testdata/role.go @@ -6,5 +6,78 @@ import ( var _ Permission +// Role can print all possible permutations of the given iterators. type Role struct { -} \ No newline at end of file + // ReturnSize is how many permissions are the returned set for the role + ReturnSize int + // N is the total number of permutations of sets this role will produce. + N int + PermissionSets []iterator + // This is kinda werird, but the first scan should not move anything. + first bool + + buffer []*Permission +} + +func NewRole(sets ...iterable) *Role { + setInterfaces := make([]iterator, 0, len(sets)) + var retSize int + var size int = 1 + for _, s := range sets { + v := s.Iterator() + setInterfaces = append(setInterfaces, v) + retSize += v.ReturnSize() + // size is the cross product of all iterator sets + size *= v.Size() + } + return &Role{ + ReturnSize: retSize, + N: size, + PermissionSets: setInterfaces, + buffer: make([]*Permission, retSize), + } +} + +// Permissions returns the set of permissions for the role for a given permutation generated by 'Next()' +func (r *Role) Permissions() Set { + var i int + for _, ps := range r.PermissionSets { + i += copy(r.buffer[i:], ps.Permissions()) + } + //all := make(Set, 0, r.ReturnSize) + //for _, set := range r.PermissionSets { + // all = append(all, set.Permissions()...) + //} + return r.buffer +} + +func (r *Role) Each(ea func(set Set)) { + for r.Next() { + ea(r.Permissions()) + } +} + +// Next will grab the next cross-product permutation of all permissions of r. +func (r *Role) Next() bool { + if !r.first { + r.first = true + return true + } + for i := range r.PermissionSets { + if r.PermissionSets[i].Next() { + break + } else { + r.PermissionSets[i].Reset() + if i == len(r.PermissionSets)-1 { + return false + } + } + } + return true +} + +func (r *Role) Reset() { + for _, ps := range r.PermissionSets { + ps.Reset() + } +} diff --git a/coderd/authz/testdata/set.go b/coderd/authz/testdata/set.go index 9ddea4c4a3df1..672c79f6d8e7a 100644 --- a/coderd/authz/testdata/set.go +++ b/coderd/authz/testdata/set.go @@ -8,6 +8,12 @@ import ( type Set []*Permission +var _ iterable = (Set)(nil) + +func (s Set) Iterator() iterator { + return union(s) +} + func (s Set) String() string { var str strings.Builder sep := "" diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/testdata/set_test.go similarity index 76% rename from coderd/authz/authztest/set_test.go rename to coderd/authz/testdata/set_test.go index 1a992fad0e743..ebafb2785cd39 100644 --- a/coderd/authz/authztest/set_test.go +++ b/coderd/authz/testdata/set_test.go @@ -1,4 +1,4 @@ -package authztest +package testdata import ( "fmt" @@ -27,7 +27,7 @@ func TestRole(t *testing.T) { { // [w] x [s1, s2, ""] = (w, s1), (w, s2), (w, "") Name: "W+", - Permutations: ParseRole(all, "w(pa) s(*e) o(*e) u(*e)"), + Permutations: ParseRole(all, "w(p) w(ae) s(pe) s(ne) s(ae) o(pe) o(ne) o(ae) u(pe) u(ne) u(ae)"), Access: true, }, { @@ -74,25 +74,25 @@ func TestRole(t *testing.T) { var total uint64 for _, c := range testCases { - total += uint64(c.Permutations.Size) + total += uint64(c.Permutations.N) } for _, c := range testCases { - fmt.Printf("%s: Size=%10d, %10f%% of total\n", - c.Name, c.Permutations.Size, 100*(float64(c.Permutations.Size)/float64(total))) + fmt.Printf("%s: N=%10d, %10f%% of total\n", + c.Name, c.Permutations.N, 100*(float64(c.Permutations.N)/float64(total))) } fmt.Printf("Total cases=%d\n", total) // This is how you run the test cases - for _, c := range testCases { - //t.Run(c.Name, func(t *testing.T) { - c.Permutations.Each(func(set Set) { - // Actually printing all the errors would be insane - //require.Equal(t, c.Access, FakeAuthorize(set)) - FakeAuthorize(set) - }) - //}) - } + //for _, c := range testCases { + //t.Run(c.Name, func(t *testing.T) { + //c.Permutations.Each(func(set Set) { + // // Actually printing all the errors would be insane + // //require.Equal(t, c.Access, FakeAuthorize(set)) + // FakeAuthorize(set) + //}) + //}) + //} } func FakeAuthorize(s Set) bool { @@ -101,8 +101,8 @@ func FakeAuthorize(s Set) bool { if i == nil { continue } - if i.Type() == "+" { - f = true + if i.Sign { + return true } } return f From e977e84b3168b5bebe31dfd10057080fdcae9cfd Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 30 Mar 2022 15:47:07 +0100 Subject: [PATCH 09/42] testdata -> authztest --- coderd/authz/{testdata => authztest}/group.go | 2 +- coderd/authz/{testdata => authztest}/group_test.go | 2 +- coderd/authz/{testdata => authztest}/iterator.go | 2 +- coderd/authz/{testdata => authztest}/level.go | 2 +- coderd/authz/{testdata => authztest}/parser.go | 2 +- coderd/authz/{testdata => authztest}/permissions.go | 2 +- coderd/authz/{testdata => authztest}/role.go | 2 +- coderd/authz/{testdata => authztest}/set.go | 2 +- coderd/authz/{testdata => authztest}/set_test.go | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) rename coderd/authz/{testdata => authztest}/group.go (97%) rename coderd/authz/{testdata => authztest}/group_test.go (97%) rename coderd/authz/{testdata => authztest}/iterator.go (98%) rename coderd/authz/{testdata => authztest}/level.go (99%) rename coderd/authz/{testdata => authztest}/parser.go (99%) rename coderd/authz/{testdata => authztest}/permissions.go (98%) rename coderd/authz/{testdata => authztest}/role.go (99%) rename coderd/authz/{testdata => authztest}/set.go (95%) rename coderd/authz/{testdata => authztest}/set_test.go (99%) diff --git a/coderd/authz/testdata/group.go b/coderd/authz/authztest/group.go similarity index 97% rename from coderd/authz/testdata/group.go rename to coderd/authz/authztest/group.go index b7fc37c786448..7191796447c90 100644 --- a/coderd/authz/testdata/group.go +++ b/coderd/authz/authztest/group.go @@ -1,4 +1,4 @@ -package testdata +package authztest type PermissionSet string diff --git a/coderd/authz/testdata/group_test.go b/coderd/authz/authztest/group_test.go similarity index 97% rename from coderd/authz/testdata/group_test.go rename to coderd/authz/authztest/group_test.go index f84e132e72fab..d7b868b138444 100644 --- a/coderd/authz/testdata/group_test.go +++ b/coderd/authz/authztest/group_test.go @@ -1,4 +1,4 @@ -package testdata +package authztest import ( "fmt" diff --git a/coderd/authz/testdata/iterator.go b/coderd/authz/authztest/iterator.go similarity index 98% rename from coderd/authz/testdata/iterator.go rename to coderd/authz/authztest/iterator.go index 31cc3920b8bde..f2fd4493752d5 100644 --- a/coderd/authz/testdata/iterator.go +++ b/coderd/authz/authztest/iterator.go @@ -1,4 +1,4 @@ -package testdata +package authztest import ( . "github.com/coder/coder/coderd/authz" diff --git a/coderd/authz/testdata/level.go b/coderd/authz/authztest/level.go similarity index 99% rename from coderd/authz/testdata/level.go rename to coderd/authz/authztest/level.go index 393dfa85e8d5a..d1bfffb96548e 100644 --- a/coderd/authz/testdata/level.go +++ b/coderd/authz/authztest/level.go @@ -1,4 +1,4 @@ -package testdata +package authztest import "github.com/coder/coder/coderd/authz" diff --git a/coderd/authz/testdata/parser.go b/coderd/authz/authztest/parser.go similarity index 99% rename from coderd/authz/testdata/parser.go rename to coderd/authz/authztest/parser.go index 9ab28a29af334..2ecd162658ff5 100644 --- a/coderd/authz/testdata/parser.go +++ b/coderd/authz/authztest/parser.go @@ -1,4 +1,4 @@ -package testdata +package authztest import ( "fmt" diff --git a/coderd/authz/testdata/permissions.go b/coderd/authz/authztest/permissions.go similarity index 98% rename from coderd/authz/testdata/permissions.go rename to coderd/authz/authztest/permissions.go index 8a889d820be2b..cb7bc3ff3abd2 100644 --- a/coderd/authz/testdata/permissions.go +++ b/coderd/authz/authztest/permissions.go @@ -1,4 +1,4 @@ -package testdata +package authztest import ( . "github.com/coder/coder/coderd/authz" diff --git a/coderd/authz/testdata/role.go b/coderd/authz/authztest/role.go similarity index 99% rename from coderd/authz/testdata/role.go rename to coderd/authz/authztest/role.go index 30c70730e5efa..7e6fddb7f2e21 100644 --- a/coderd/authz/testdata/role.go +++ b/coderd/authz/authztest/role.go @@ -1,4 +1,4 @@ -package testdata +package authztest import ( . "github.com/coder/coder/coderd/authz" diff --git a/coderd/authz/testdata/set.go b/coderd/authz/authztest/set.go similarity index 95% rename from coderd/authz/testdata/set.go rename to coderd/authz/authztest/set.go index 672c79f6d8e7a..8e9b668db94cf 100644 --- a/coderd/authz/testdata/set.go +++ b/coderd/authz/authztest/set.go @@ -1,4 +1,4 @@ -package testdata +package authztest import ( "strings" diff --git a/coderd/authz/testdata/set_test.go b/coderd/authz/authztest/set_test.go similarity index 99% rename from coderd/authz/testdata/set_test.go rename to coderd/authz/authztest/set_test.go index ebafb2785cd39..e4d1d208503a4 100644 --- a/coderd/authz/testdata/set_test.go +++ b/coderd/authz/authztest/set_test.go @@ -1,4 +1,4 @@ -package testdata +package authztest import ( "fmt" From 00a7c3fa2eba0b0511bd5a746037eb7ed5c76663 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 30 Mar 2022 15:55:29 +0100 Subject: [PATCH 10/42] WIP: start work on SVO --- coderd/authz/object.go | 12 ++++++++++++ coderd/authz/resources.go | 9 +++++++++ coderd/authz/subject.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 coderd/authz/object.go create mode 100644 coderd/authz/resources.go create mode 100644 coderd/authz/subject.go diff --git a/coderd/authz/object.go b/coderd/authz/object.go new file mode 100644 index 0000000000000..c290161de7074 --- /dev/null +++ b/coderd/authz/object.go @@ -0,0 +1,12 @@ +package authz + +// Object is the resource being accessed +type Object struct { + ObjectID string `json:"object_id"` + OwnerID string `json:"owner_id"` + OrgOwnerID string `json:"org_owner_id"` + + // ObjectType is "workspace", "project", "devurl", etc + ObjectType ResourceType `json:"object_type"` + // TODO: SharedUsers? +} diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go new file mode 100644 index 0000000000000..b183ee221ad92 --- /dev/null +++ b/coderd/authz/resources.go @@ -0,0 +1,9 @@ +package authz + +type ResourceType string + +const ( + ResourceTypeWorkspace = "workspace" + ResourceTypeProject = "project" + ResourceTypeDevURL = "devurl" +) diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go new file mode 100644 index 0000000000000..1343576c69d3a --- /dev/null +++ b/coderd/authz/subject.go @@ -0,0 +1,28 @@ +package authz + +// +//// Subject is the actor that is performing the action on an object +//type Subject struct { +// UserID string `json:"user_id"` +// +// SiteRoles []Role `json:"site_roles"` +// +// // Ops are mapped for the resource and the list of operations on the resource for the scope. +// SiteOps []Permission `json:"site_ops"` +// OrgOps []Permission `json:"org_ops"` +// // UserOps only affect objects owned by the user +// UserOps []Permission `json:"user_ops"` +//} +// +//func (s Subject) AllPermissions() []Permission{ +// // Explosion of roles + scopes +// return []Permission{} +//} +// +//// Authn +//type S struct { +// SiteRoles() ([]rbac.Roles, error) +// OrgRoles(ctx context.Context, orgID string) ([]rbac.Roles, error) +// UserRoles() ([]rbac.Roles, error) +// Scopes() ([]rbac.ResourcePermission, error) +//} From 1f04c011ca514482d2f97a7d2bf7bba823809d47 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 31 Mar 2022 11:07:58 -0500 Subject: [PATCH 11/42] reduce allocations for union sets --- coderd/authz/authztest/iterator.go | 61 ++++++++++++++++++------------ 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/authztest/iterator.go index f2fd4493752d5..d3c95860cc90b 100644 --- a/coderd/authz/authztest/iterator.go +++ b/coderd/authz/authztest/iterator.go @@ -18,52 +18,65 @@ type iterator interface { Size() int } -// SetIterator is very primitive, just used to hold a place in a set. -type SetIterator struct { - i int - set Set +// unionIterator is very primitive, just used to hold a place in a set. +type unionIterator struct { + // setIdx determines which set the offset is for + setIdx int + // offset is which permission for a given setIdx + offset int + sets []Set + // buffer is used to prevent allocations when `Permissions` is called, as we must + // return a set. buffer Set + + N int } -func union(sets ...Set) *SetIterator { - all := Set{} - for _, set := range sets { - all = append(all, set...) +func union(sets ...Set) *unionIterator { + var n int + for _, s := range sets { + n += len(s) } - return &SetIterator{ - i: 0, - set: all, + return &unionIterator{ + sets: sets, buffer: make(Set, 1), + N: n, } } -func (si *SetIterator) Next() bool { - si.i++ - return si.i < len(si.set) +func (si *unionIterator) Next() bool { + si.offset++ + if si.offset >= len(si.sets[si.setIdx]) { + si.setIdx++ + si.offset = 0 + } + + return si.setIdx >= len(si.sets) } -func (si *SetIterator) Permissions() Set { - si.buffer[0] = si.set[si.i] +func (si *unionIterator) Permissions() Set { + si.buffer[0] = si.Permission() return si.buffer } -func (si *SetIterator) Permission() *Permission { - return si.set[si.i] +func (si unionIterator) Permission() *Permission { + return si.sets[si.setIdx][si.offset] } -func (si *SetIterator) Reset() { - si.i = 0 +func (si *unionIterator) Reset() { + si.setIdx = 0 + si.offset = 0 } -func (si *SetIterator) ReturnSize() int { +func (si *unionIterator) ReturnSize() int { return 1 } -func (si *SetIterator) Size() int { - return len(si.set) +func (si *unionIterator) Size() int { + return si.N } -func (si *SetIterator) Iterator() iterator { +func (si *unionIterator) Iterator() iterator { return si } From fbf4db17a76079e5aaf4f08794e4b3d0c0226e79 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 31 Mar 2022 11:11:32 -0500 Subject: [PATCH 12/42] fix: Fix nil permissions as Strings() --- coderd/authz/authztest/group.go | 1 + coderd/authz/authztest/group_test.go | 6 ++++++ coderd/authz/authztest/iterator.go | 2 +- coderd/authz/authztest/role.go | 4 ---- coderd/authz/authztest/set.go | 3 +++ 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/coderd/authz/authztest/group.go b/coderd/authz/authztest/group.go index 7191796447c90..a8d8273026652 100644 --- a/coderd/authz/authztest/group.go +++ b/coderd/authz/authztest/group.go @@ -1,5 +1,6 @@ package authztest +// PermissionSet defines a set of permissions with the same impact. type PermissionSet string const ( diff --git a/coderd/authz/authztest/group_test.go b/coderd/authz/authztest/group_test.go index d7b868b138444..7d0d70f8d6d42 100644 --- a/coderd/authz/authztest/group_test.go +++ b/coderd/authz/authztest/group_test.go @@ -28,4 +28,10 @@ func Test_PermissionSetWPlusSearchSpace(t *testing.T) { for k, v := range all { fmt.Printf("%s=%d\n", string(k), len(v.All())) } + + var i int + wplus.Each(func(set Set) { + fmt.Printf("%d: %s\n", i, set.String()) + i++ + }) } diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/authztest/iterator.go index d3c95860cc90b..91820e1ade1f8 100644 --- a/coderd/authz/authztest/iterator.go +++ b/coderd/authz/authztest/iterator.go @@ -51,7 +51,7 @@ func (si *unionIterator) Next() bool { si.offset = 0 } - return si.setIdx >= len(si.sets) + return si.setIdx < len(si.sets) } func (si *unionIterator) Permissions() Set { diff --git a/coderd/authz/authztest/role.go b/coderd/authz/authztest/role.go index 7e6fddb7f2e21..3c2720540e769 100644 --- a/coderd/authz/authztest/role.go +++ b/coderd/authz/authztest/role.go @@ -44,10 +44,6 @@ func (r *Role) Permissions() Set { for _, ps := range r.PermissionSets { i += copy(r.buffer[i:], ps.Permissions()) } - //all := make(Set, 0, r.ReturnSize) - //for _, set := range r.PermissionSets { - // all = append(all, set.Permissions()...) - //} return r.buffer } diff --git a/coderd/authz/authztest/set.go b/coderd/authz/authztest/set.go index 8e9b668db94cf..ef009b6064e62 100644 --- a/coderd/authz/authztest/set.go +++ b/coderd/authz/authztest/set.go @@ -18,6 +18,9 @@ func (s Set) String() string { var str strings.Builder sep := "" for _, v := range s { + if v == nil { + continue + } str.WriteString(sep) str.WriteString(v.String()) sep = ", " From 494689732802933495cada7dd6365e18f12a810d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 31 Mar 2022 11:57:38 -0500 Subject: [PATCH 13/42] chore: Make all permission variant levels Playing around with helper functions to make life easy --- coderd/authz/authz.go | 6 ++ coderd/authz/authz_test.go | 150 ++++++++++++++++++++++++++ coderd/authz/authztest/group.go | 20 ++-- coderd/authz/authztest/group_test.go | 20 ++-- coderd/authz/authztest/iterator.go | 43 ++++++-- coderd/authz/authztest/level.go | 40 +++---- coderd/authz/authztest/parser.go | 6 +- coderd/authz/authztest/permissions.go | 19 ++-- coderd/authz/authztest/role.go | 26 +++-- coderd/authz/authztest/set.go | 6 +- 10 files changed, 265 insertions(+), 71 deletions(-) create mode 100644 coderd/authz/authz.go create mode 100644 coderd/authz/authz_test.go diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go new file mode 100644 index 0000000000000..b9c6a2c7fe5cc --- /dev/null +++ b/coderd/authz/authz.go @@ -0,0 +1,6 @@ +package authz + +// TODO: Implement Authorize +func Authorize(subj interface{}, obj Object, action interface{}) error { + return nil +} diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go new file mode 100644 index 0000000000000..57813b807f377 --- /dev/null +++ b/coderd/authz/authz_test.go @@ -0,0 +1,150 @@ +package authz_test + +import ( + "fmt" + "github.com/coder/coder/coderd/authz/authztest" + "math/bits" + "testing" +) + +var nilSet = authztest.Set{nil} + +func Test_ExhaustiveAuthorize(t *testing.T) { + all := authztest.GroupedPermissions(authztest.AllPermissions()) + variants := permissionVariants(all) + for name, v := range variants { + fmt.Printf("%s: %d\n", name, v.Size()) + } +} + +func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { + // an is any noise above the impactful set + an := abstain + // ln is any noise below the impactful set + ln := positive | negative | abstain + + // Cases are X+/- where X indicates the level where the impactful set is. + // The impactful set determines the result. + return map[string]*authztest.Role{ + // Wild + "W+": authztest.NewRole( + pos(all.Wildcard()), + noise(ln, all.Site(), all.Org(), all.User()), + ), + "W-": authztest.NewRole( + neg(all.Wildcard()), + noise(ln, all.Site(), all.Org(), all.User()), + ), + // Site + "S+": authztest.NewRole( + noise(an, all.Wildcard()), + pos(all.Site()), + noise(ln, all.Org(), all.User()), + ), + "S-": authztest.NewRole( + noise(an, all.Wildcard()), + neg(all.Site()), + noise(ln, all.Org(), all.User()), + ), + // TODO: Figure out cross org noise between org:* and org:mem + // Org:* + "O+": authztest.NewRole( + noise(an, all.Wildcard(), all.Site()), + pos(all.Org()), + noise(ln, all.User()), + ), + "O-": authztest.NewRole( + noise(an, all.Wildcard(), all.Site()), + neg(all.Org()), + noise(ln, all.User()), + ), + // Org:Mem + "M+": authztest.NewRole( + noise(an, all.Wildcard(), all.Site()), + pos(all.OrgMem()), + noise(ln, all.User()), + ), + "M-": authztest.NewRole( + noise(an, all.Wildcard(), all.Site()), + neg(all.OrgMem()), + noise(ln, all.User()), + ), + // User + "U+": authztest.NewRole( + noise(an, all.Wildcard(), all.Site(), all.Org()), + pos(all.User()), + ), + "U-": authztest.NewRole( + noise(an, all.Wildcard(), all.Site(), all.Org()), + neg(all.User()), + ), + } +} + +func l() { + //authztest.Levels + //noise(an, all.Wildcard()), + // neg(all.Site()), + // noise(ln, all.Org(), all.User()), +} + +// pos returns the positive impactful variant for a given level. It does not +// include noise at any other level but the one given. +func pos(lvl authztest.LevelGroup) *authztest.Role { + return authztest.NewRole( + lvl.Positive(), + authztest.Union(lvl.Abstain()[:1], nilSet), + ) +} + +func neg(lvl authztest.LevelGroup) *authztest.Role { + return authztest.NewRole( + lvl.Negative(), + authztest.Union(lvl.Positive()[:1], nilSet), + authztest.Union(lvl.Abstain()[:1], nilSet), + ) +} + +type noiseBits uint8 + +const ( + none noiseBits = 1 << iota + positive + negative + abstain +) + +func flagMatch(flag, in noiseBits) bool { + return flag&in != 0 +} + +// noise returns the noise permission permutations for a given level. You can +// use this helper function when this level is not impactful. +// The returned role is the permutations including at least one example of +// positive, negative, and neutral permissions. It also includes the set of +// no additional permissions. +func noise(f noiseBits, lvls ...authztest.LevelGroup) *authztest.Role { + rs := make([]authztest.Iterable, 0, len(lvls)) + for _, lvl := range lvls { + sets := make([]authztest.Iterable, 0, bits.OnesCount8(uint8(f))) + + if flagMatch(positive, f) { + sets = append(sets, authztest.Union(lvl.Positive()[:1], nilSet)) + } + if flagMatch(negative, f) { + sets = append(sets, authztest.Union(lvl.Negative()[:1], nilSet)) + } + if flagMatch(abstain, f) { + sets = append(sets, authztest.Union(lvl.Abstain()[:1], nilSet)) + } + + rs = append(rs, authztest.NewRole( + sets..., + )) + } + + if len(rs) == 1 { + return rs[0].(*authztest.Role) + } + return authztest.NewRole(rs...) +} diff --git a/coderd/authz/authztest/group.go b/coderd/authz/authztest/group.go index a8d8273026652..45d770538281b 100644 --- a/coderd/authz/authztest/group.go +++ b/coderd/authz/authztest/group.go @@ -18,17 +18,17 @@ var nilSet = Set{nil} // *.*.*.* //var PermissionSetWPlus = NewRole( // all.Wildcard().Positive(), -// union(all.Wildcard().Abstain(), nilSet), +// Union(all.Wildcard().Abstain(), nilSet), // -// union(all.Site().Positive(), nilSet), -// union(all.Site().Negative(), nilSet), -// union(all.Site().Abstain(), nilSet), +// Union(all.Site().Positive(), nilSet), +// Union(all.Site().Negative(), nilSet), +// Union(all.Site().Abstain(), nilSet), // -// union(all.AllOrgs().Positive(), nilSet), -// union(all.AllOrgs().Negative(), nilSet), -// union(all.AllOrgs().Abstain(), nilSet), +// Union(all.AllOrgs().Positive(), nilSet), +// Union(all.AllOrgs().Negative(), nilSet), +// Union(all.AllOrgs().Abstain(), nilSet), // -// union(all.User().Positive(), nilSet), -// union(all.User().Negative(), nilSet), -// union(all.User().Abstain(), nilSet), +// Union(all.User().Positive(), nilSet), +// Union(all.User().Negative(), nilSet), +// Union(all.User().Abstain(), nilSet), //) diff --git a/coderd/authz/authztest/group_test.go b/coderd/authz/authztest/group_test.go index 7d0d70f8d6d42..a7cd1e94f07cc 100644 --- a/coderd/authz/authztest/group_test.go +++ b/coderd/authz/authztest/group_test.go @@ -9,19 +9,19 @@ func Test_PermissionSetWPlusSearchSpace(t *testing.T) { all := GroupedPermissions(AllPermissions()) wplus := NewRole( all.Wildcard().Positive(), - union(all.Wildcard().Abstain()[:1], nilSet), + Union(all.Wildcard().Abstain()[:1], nilSet), - union(all.Site().Positive()[:1], nilSet), - union(all.Site().Negative()[:1], nilSet), - union(all.Site().Abstain()[:1], nilSet), + Union(all.Site().Positive()[:1], nilSet), + Union(all.Site().Negative()[:1], nilSet), + Union(all.Site().Abstain()[:1], nilSet), - union(all.AllOrgs().Positive()[:1], nilSet), - union(all.AllOrgs().Negative()[:1], nilSet), - union(all.AllOrgs().Abstain()[:1], nilSet), + Union(all.AllOrgs().Positive()[:1], nilSet), + Union(all.AllOrgs().Negative()[:1], nilSet), + Union(all.AllOrgs().Abstain()[:1], nilSet), - union(all.User().Positive()[:1], nilSet), - union(all.User().Negative()[:1], nilSet), - union(all.User().Abstain()[:1], nilSet), + Union(all.User().Positive()[:1], nilSet), + Union(all.User().Negative()[:1], nilSet), + Union(all.User().Abstain()[:1], nilSet), ) fmt.Println(wplus.N) fmt.Println(len(AllPermissions())) diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/authztest/iterator.go index 91820e1ade1f8..b746f9e92e23f 100644 --- a/coderd/authz/authztest/iterator.go +++ b/coderd/authz/authztest/iterator.go @@ -4,12 +4,12 @@ import ( . "github.com/coder/coder/coderd/authz" ) -type iterable interface { - Iterator() iterator +type Iterable interface { + Iterator() Iterator } -type iterator interface { - iterable +type Iterator interface { + Iterable Next() bool Permissions() Set @@ -32,7 +32,7 @@ type unionIterator struct { N int } -func union(sets ...Set) *unionIterator { +func Union(sets ...Set) *unionIterator { var n int for _, s := range sets { n += len(s) @@ -76,10 +76,37 @@ func (si *unionIterator) Size() int { return si.N } -func (si *unionIterator) Iterator() iterator { +func (si *unionIterator) Iterator() Iterator { return si } +type productI struct { + ReturnSize int + N int + PermissionSets []Iterator + + buffer Set +} + +func ProductI(sets ...Iterable) *productI { + setInterfaces := make([]Iterator, 0, len(sets)) + var retSize int + var size int = 1 + for _, s := range sets { + v := s.Iterator() + setInterfaces = append(setInterfaces, v) + retSize += v.ReturnSize() + // size is the cross product of all Iterator sets + size *= v.Size() + } + return &productI{ + ReturnSize: retSize, + N: size, + PermissionSets: setInterfaces, + buffer: make([]*Permission, retSize), + } +} + type productIterator struct { i, j int a Set @@ -87,7 +114,7 @@ type productIterator struct { buffer Set } -func product(a, b Set) *productIterator { +func Product(a, b Set) *productIterator { i := &productIterator{ i: 0, j: 0, @@ -128,6 +155,6 @@ func (s *productIterator) Size() int { return len(s.a) * len(s.b) } -func (s *productIterator) Iterator() iterator { +func (s *productIterator) Iterator() Iterator { return s } diff --git a/coderd/authz/authztest/level.go b/coderd/authz/authztest/level.go index d1bfffb96548e..a78b44be9fc2c 100644 --- a/coderd/authz/authztest/level.go +++ b/coderd/authz/authztest/level.go @@ -5,13 +5,13 @@ import "github.com/coder/coder/coderd/authz" type level string const ( - levelWild level = "level-wild" - levelSite level = "level-site" - levelOrg level = "level-org" - levelOrgMem level = "level-org:mem" - // levelOrgAll is a helper to get both org levels above - levelOrgAll level = "level-org:*" - levelUser level = "level-user" + LevelWildKey level = "level-wild" + LevelSiteKey level = "level-site" + LevelOrgKey level = "level-org" + LevelOrgMemKey level = "level-org:mem" + // LevelOrgAllKey is a helper to get both org levels above + LevelOrgAllKey level = "level-org:*" + LevelUserKey level = "level-user" ) // LevelGroup is all permissions for a given level @@ -43,7 +43,7 @@ func (lg LevelGroup) Abstain() Set { func GroupedPermissions(perms Set) SetGroup { groups := make(SetGroup) - allLevelKeys := []level{levelWild, levelSite, levelOrg, levelOrgMem, levelOrgAll, levelUser} + allLevelKeys := []level{LevelWildKey, LevelSiteKey, LevelOrgKey, LevelOrgMemKey, LevelOrgAllKey, LevelUserKey} for _, l := range allLevelKeys { groups[l] = make(LevelGroup) @@ -53,18 +53,18 @@ func GroupedPermissions(perms Set) SetGroup { m := Impact(p) switch { case p.Level == authz.LevelSite: - groups[levelSite][m] = append(groups[levelSite][m], p) + groups[LevelSiteKey][m] = append(groups[LevelSiteKey][m], p) case p.Level == authz.LevelOrg: - groups[levelOrgAll][m] = append(groups[levelOrgAll][m], p) + groups[LevelOrgAllKey][m] = append(groups[LevelOrgAllKey][m], p) if p.LevelID == "" || p.LevelID == "*" { - groups[levelOrg][m] = append(groups[levelOrg][m], p) + groups[LevelOrgKey][m] = append(groups[LevelOrgKey][m], p) } else { - groups[levelOrgMem][m] = append(groups[levelOrgMem][m], p) + groups[LevelOrgMemKey][m] = append(groups[LevelOrgMemKey][m], p) } case p.Level == authz.LevelUser: - groups[levelUser][m] = append(groups[levelUser][m], p) + groups[LevelUserKey][m] = append(groups[LevelUserKey][m], p) case p.Level == authz.LevelWildcard: - groups[levelWild][m] = append(groups[levelWild][m], p) + groups[LevelWildKey][m] = append(groups[LevelWildKey][m], p) } } @@ -74,25 +74,25 @@ func GroupedPermissions(perms Set) SetGroup { type SetGroup map[level]LevelGroup func (s SetGroup) Wildcard() LevelGroup { - return s[levelWild] + return s[LevelWildKey] } func (s SetGroup) Site() LevelGroup { - return s[levelSite] + return s[LevelSiteKey] } func (s SetGroup) Org() LevelGroup { - return s[levelOrg] + return s[LevelOrgKey] } func (s SetGroup) AllOrgs() LevelGroup { - return s[levelOrgAll] + return s[LevelOrgAllKey] } func (s SetGroup) OrgMem() LevelGroup { - return s[levelOrgMem] + return s[LevelOrgMemKey] } func (s SetGroup) User() LevelGroup { - return s[levelUser] + return s[LevelUserKey] } diff --git a/coderd/authz/authztest/parser.go b/coderd/authz/authztest/parser.go index 2ecd162658ff5..889fe2b5ce201 100644 --- a/coderd/authz/authztest/parser.go +++ b/coderd/authz/authztest/parser.go @@ -9,7 +9,7 @@ type Parser struct { stack []interface{} grp SetGroup - setI []iterable + setI []Iterable } func ParseRole(grp SetGroup, input string) *Role { @@ -18,7 +18,7 @@ func ParseRole(grp SetGroup, input string) *Role { return NewRole(p.setI...) } -func Parse(grp SetGroup, input string) []iterable { +func Parse(grp SetGroup, input string) []Iterable { p := NewParser(grp, input) p.parse() return p.setI @@ -110,7 +110,7 @@ func (p *Parser) handleLevel(l uint8, ptr int) int { } ptr++ if stop { - p.setI = append(p.setI, union(sets...)) + p.setI = append(p.setI, Union(sets...)) return ptr } } diff --git a/coderd/authz/authztest/permissions.go b/coderd/authz/authztest/permissions.go index cb7bc3ff3abd2..f5b7e78290f47 100644 --- a/coderd/authz/authztest/permissions.go +++ b/coderd/authz/authztest/permissions.go @@ -9,22 +9,21 @@ const ( ) var ( - Levels = PermissionLevels - LevelIDs = []string{"", "mem"} - ResourceTypes = []string{"resource", "*", otherOption} - ResourceIDs = []string{"rid", "*", otherOption} - Actions = []string{"action", "*", otherOption} + levelIDs = []string{"", "mem"} + resourceTypes = []string{"resource", "*", otherOption} + resourceIDs = []string{"rid", "*", otherOption} + actions = []string{"action", "*", otherOption} ) // AllPermissions returns all the possible permissions ever. func AllPermissions() Set { permissionTypes := []bool{true, false} - all := make(Set, 0, len(permissionTypes)*len(Levels)*len(LevelIDs)*len(ResourceTypes)*len(ResourceIDs)*len(Actions)) + all := make(Set, 0, len(permissionTypes)*len(PermissionLevels)*len(levelIDs)*len(resourceTypes)*len(resourceIDs)*len(actions)) for _, s := range permissionTypes { - for _, l := range Levels { - for _, t := range ResourceTypes { - for _, i := range ResourceIDs { - for _, a := range Actions { + for _, l := range PermissionLevels { + for _, t := range resourceTypes { + for _, i := range resourceIDs { + for _, a := range actions { if l == LevelOrg { all = append(all, &Permission{ Sign: s, diff --git a/coderd/authz/authztest/role.go b/coderd/authz/authztest/role.go index 3c2720540e769..8a38e77dfa5d5 100644 --- a/coderd/authz/authztest/role.go +++ b/coderd/authz/authztest/role.go @@ -8,36 +8,48 @@ var _ Permission // Role can print all possible permutations of the given iterators. type Role struct { - // ReturnSize is how many permissions are the returned set for the role - ReturnSize int + // returnSize is how many permissions are the returned set for the role + returnSize int // N is the total number of permutations of sets this role will produce. N int - PermissionSets []iterator + PermissionSets []Iterator // This is kinda werird, but the first scan should not move anything. first bool buffer []*Permission } -func NewRole(sets ...iterable) *Role { - setInterfaces := make([]iterator, 0, len(sets)) +func NewRole(sets ...Iterable) *Role { + setInterfaces := make([]Iterator, 0, len(sets)) var retSize int var size int = 1 for _, s := range sets { v := s.Iterator() setInterfaces = append(setInterfaces, v) retSize += v.ReturnSize() - // size is the cross product of all iterator sets + // size is the cross product of all Iterator sets size *= v.Size() } return &Role{ - ReturnSize: retSize, + returnSize: retSize, N: size, PermissionSets: setInterfaces, buffer: make([]*Permission, retSize), } } +func (r *Role) Iterator() Iterator { + return r +} + +func (r *Role) ReturnSize() int { + return r.returnSize +} + +func (r *Role) Size() int { + return r.N +} + // Permissions returns the set of permissions for the role for a given permutation generated by 'Next()' func (r *Role) Permissions() Set { var i int diff --git a/coderd/authz/authztest/set.go b/coderd/authz/authztest/set.go index ef009b6064e62..df8e4c3d7d9d2 100644 --- a/coderd/authz/authztest/set.go +++ b/coderd/authz/authztest/set.go @@ -8,10 +8,10 @@ import ( type Set []*Permission -var _ iterable = (Set)(nil) +var _ Iterable = (Set)(nil) -func (s Set) Iterator() iterator { - return union(s) +func (s Set) Iterator() Iterator { + return Union(s) } func (s Set) String() string { From 7e6cc664696c1a456e151827b04460433a959fd2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 31 Mar 2022 14:01:05 -0500 Subject: [PATCH 14/42] First full draft of the authz authorize test --- coderd/authz/action.go | 10 ++ coderd/authz/authz.go | 8 +- coderd/authz/authz_test.go | 137 +++++++++++++++++++++----- coderd/authz/authztest/iterator.go | 6 +- coderd/authz/authztest/object.go | 16 +++ coderd/authz/authztest/permissions.go | 31 +++--- coderd/authz/authztest/role.go | 13 +-- coderd/authz/authztest/set.go | 14 ++- coderd/authz/role.go | 6 ++ coderd/authz/subject.go | 61 +++++++----- 10 files changed, 223 insertions(+), 79 deletions(-) create mode 100644 coderd/authz/action.go create mode 100644 coderd/authz/authztest/object.go create mode 100644 coderd/authz/role.go diff --git a/coderd/authz/action.go b/coderd/authz/action.go new file mode 100644 index 0000000000000..0597b56916d09 --- /dev/null +++ b/coderd/authz/action.go @@ -0,0 +1,10 @@ +package authz + +type Action string + +const ( + ReadAction = "read" + WriteAction = "write" + ModifyAction = "modify" + DeleteAction = "delete" +) diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index b9c6a2c7fe5cc..029eb8a831db9 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -1,6 +1,12 @@ package authz // TODO: Implement Authorize -func Authorize(subj interface{}, obj Object, action interface{}) error { +func Authorize(subj Subject, obj Object, action Action) error { + // TODO: Expand subject roles into their permissions as appropriate. Apply scopes. + return AuthorizePermissions(subj.ID(), []Permission{}, obj, action) +} + +// AuthorizePermissions runs the authorize function with the raw permissions in a single list. +func AuthorizePermissions(subjID string, permissions []Permission, object Object, action Action) error { return nil } diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 57813b807f377..f13c4166a82d1 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -2,8 +2,10 @@ package authz_test import ( "fmt" + "github.com/coder/coder/coderd/authz" "github.com/coder/coder/coderd/authz/authztest" "math/bits" + "strings" "testing" ) @@ -11,17 +13,109 @@ var nilSet = authztest.Set{nil} func Test_ExhaustiveAuthorize(t *testing.T) { all := authztest.GroupedPermissions(authztest.AllPermissions()) - variants := permissionVariants(all) - for name, v := range variants { + roleVariants := permissionVariants(all) + + testCases := []struct { + Name string + Objs []authz.Object + // Action is constant + // Subject comes from roleVariants + Result func(pv string) bool + }{ + { + Name: "User:Org", + Objs: authztest.Objects( + []string{authztest.PermMe, authztest.PermOrgID}, + ), + Result: func(pv string) bool { + return strings.Contains(pv, "+") + }, + }, + { + // All U+/- tests should fail + Name: "NotUser:Org", + Objs: authztest.Objects( + []string{"other", authztest.PermOrgID}, + []string{"", authztest.PermOrgID}, + ), + Result: func(pv string) bool { + if strings.Contains(pv, "U") { + return false + } + return strings.Contains(pv, "+") + }, + }, + { + // All O+/- and U+/- tests should fail + Name: "NotUser:NotOrg", + Objs: authztest.Objects( + []string{authztest.PermMe, "non-mem"}, + []string{"other", "non-mem"}, + []string{"other", ""}, + []string{"", "non-mem"}, + []string{"", ""}, + ), + Result: func(pv string) bool { + if strings.Contains(pv, "U") { + return false + } + if strings.Contains(pv, "O") { + return false + } + return strings.Contains(pv, "+") + }, + }, + // TODO: @emyrk for this one, we should probably pass a custom roles variant + //{ + // // O+, O- no longer pass judgement. Defer to user level judgement (only somewhat tricky case) + // Name: "User:NotOrg", + // Objs: authztest.Objects( + // []string{authztest.PermMe, ""}, + // ), + // Result: func(pv string) bool { + // return strings.Contains(pv, "+") + // }, + //}, + } + + var pvars int + for name, v := range roleVariants { fmt.Printf("%s: %d\n", name, v.Size()) + pvars += v.Size() + } + var total int = 0 + for _, c := range testCases { + total += len(c.Objs) * pvars + } + fmt.Printf("pvars=%d, total=%d\n", pvars, total) + + var tot int + for _, c := range testCases { + t.Run(c.Name, func(t *testing.T) { + for _, o := range c.Objs { + for _, v := range roleVariants { + v.Each(func(set authztest.Set) { + // TODO: Authz.Permissions does allocations at the moment. We should fix that. + err := authz.AuthorizePermissions( + authztest.PermMe, + set.Permissions(), + o, + authztest.PermAction) + var _ = err + tot++ + }) + v.Reset() + } + } + }) } } func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { // an is any noise above the impactful set - an := abstain + an := noiseAbstain // ln is any noise below the impactful set - ln := positive | negative | abstain + ln := noisePositive | noiseNegative | noiseAbstain // Cases are X+/- where X indicates the level where the impactful set is. // The impactful set determines the result. @@ -46,26 +140,25 @@ func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { neg(all.Site()), noise(ln, all.Org(), all.User()), ), - // TODO: Figure out cross org noise between org:* and org:mem - // Org:* + // Org:* -- Added org:mem noise "O+": authztest.NewRole( - noise(an, all.Wildcard(), all.Site()), + noise(an, all.Wildcard(), all.Site(), all.OrgMem()), pos(all.Org()), noise(ln, all.User()), ), "O-": authztest.NewRole( - noise(an, all.Wildcard(), all.Site()), + noise(an, all.Wildcard(), all.Site(), all.OrgMem()), neg(all.Org()), noise(ln, all.User()), ), - // Org:Mem + // Org:Mem -- Added org:* noise "M+": authztest.NewRole( - noise(an, all.Wildcard(), all.Site()), + noise(an, all.Wildcard(), all.Site(), all.Org()), pos(all.OrgMem()), noise(ln, all.User()), ), "M-": authztest.NewRole( - noise(an, all.Wildcard(), all.Site()), + noise(an, all.Wildcard(), all.Site(), all.Org()), neg(all.OrgMem()), noise(ln, all.User()), ), @@ -78,16 +171,10 @@ func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { noise(an, all.Wildcard(), all.Site(), all.Org()), neg(all.User()), ), + // TODO: @Emyrk the abstain sets } } -func l() { - //authztest.Levels - //noise(an, all.Wildcard()), - // neg(all.Site()), - // noise(ln, all.Org(), all.User()), -} - // pos returns the positive impactful variant for a given level. It does not // include noise at any other level but the one given. func pos(lvl authztest.LevelGroup) *authztest.Role { @@ -108,10 +195,10 @@ func neg(lvl authztest.LevelGroup) *authztest.Role { type noiseBits uint8 const ( - none noiseBits = 1 << iota - positive - negative - abstain + _ noiseBits = 1 << iota + noisePositive + noiseNegative + noiseAbstain ) func flagMatch(flag, in noiseBits) bool { @@ -128,13 +215,13 @@ func noise(f noiseBits, lvls ...authztest.LevelGroup) *authztest.Role { for _, lvl := range lvls { sets := make([]authztest.Iterable, 0, bits.OnesCount8(uint8(f))) - if flagMatch(positive, f) { + if flagMatch(noisePositive, f) { sets = append(sets, authztest.Union(lvl.Positive()[:1], nilSet)) } - if flagMatch(negative, f) { + if flagMatch(noiseNegative, f) { sets = append(sets, authztest.Union(lvl.Negative()[:1], nilSet)) } - if flagMatch(abstain, f) { + if flagMatch(noiseAbstain, f) { sets = append(sets, authztest.Union(lvl.Abstain()[:1], nilSet)) } diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/authztest/iterator.go index b746f9e92e23f..e58e942d9eb6b 100644 --- a/coderd/authz/authztest/iterator.go +++ b/coderd/authz/authztest/iterator.go @@ -1,7 +1,7 @@ package authztest import ( - . "github.com/coder/coder/coderd/authz" + "github.com/coder/coder/coderd/authz" ) type Iterable interface { @@ -59,7 +59,7 @@ func (si *unionIterator) Permissions() Set { return si.buffer } -func (si unionIterator) Permission() *Permission { +func (si unionIterator) Permission() *authz.Permission { return si.sets[si.setIdx][si.offset] } @@ -103,7 +103,7 @@ func ProductI(sets ...Iterable) *productI { ReturnSize: retSize, N: size, PermissionSets: setInterfaces, - buffer: make([]*Permission, retSize), + buffer: make([]*authz.Permission, retSize), } } diff --git a/coderd/authz/authztest/object.go b/coderd/authz/authztest/object.go new file mode 100644 index 0000000000000..21c407e11acbc --- /dev/null +++ b/coderd/authz/authztest/object.go @@ -0,0 +1,16 @@ +package authztest + +import "github.com/coder/coder/coderd/authz" + +func Objects(pairs ...[]string) []authz.Object { + objs := make([]authz.Object, 0, len(pairs)) + for _, p := range pairs { + objs = append(objs, authz.Object{ + ObjectID: PermObjectID, + OwnerID: p[0], + OrgOwnerID: p[1], + ObjectType: PermObjectType, + }) + } + return objs +} diff --git a/coderd/authz/authztest/permissions.go b/coderd/authz/authztest/permissions.go index f5b7e78290f47..36abadfeb5eea 100644 --- a/coderd/authz/authztest/permissions.go +++ b/coderd/authz/authztest/permissions.go @@ -1,40 +1,45 @@ package authztest import ( - . "github.com/coder/coder/coderd/authz" + "github.com/coder/coder/coderd/authz" ) const ( - otherOption = "other" + otherOption = "other" + PermObjectType = "resource" + PermAction = "read" + PermOrgID = "mem" + PermObjectID = "rid" + PermMe = "me" ) var ( - levelIDs = []string{"", "mem"} - resourceTypes = []string{"resource", "*", otherOption} - resourceIDs = []string{"rid", "*", otherOption} - actions = []string{"action", "*", otherOption} + levelIDs = []string{"", PermOrgID} + resourceTypes = []string{PermObjectType, "*", otherOption} + resourceIDs = []string{PermObjectID, "*", otherOption} + actions = []string{PermAction, "*", otherOption} ) // AllPermissions returns all the possible permissions ever. func AllPermissions() Set { permissionTypes := []bool{true, false} - all := make(Set, 0, len(permissionTypes)*len(PermissionLevels)*len(levelIDs)*len(resourceTypes)*len(resourceIDs)*len(actions)) + all := make(Set, 0, len(permissionTypes)*len(authz.PermissionLevels)*len(levelIDs)*len(resourceTypes)*len(resourceIDs)*len(actions)) for _, s := range permissionTypes { - for _, l := range PermissionLevels { + for _, l := range authz.PermissionLevels { for _, t := range resourceTypes { for _, i := range resourceIDs { for _, a := range actions { - if l == LevelOrg { - all = append(all, &Permission{ + if l == authz.LevelOrg { + all = append(all, &authz.Permission{ Sign: s, Level: l, - LevelID: "mem", + LevelID: PermOrgID, ResourceType: t, ResourceID: i, Action: a, }) } - all = append(all, &Permission{ + all = append(all, &authz.Permission{ Sign: s, Level: l, LevelID: "", @@ -51,7 +56,7 @@ func AllPermissions() Set { } // Impact returns the impact (positive, negative, abstain) of p -func Impact(p *Permission) PermissionSet { +func Impact(p *authz.Permission) PermissionSet { if p.ResourceType == otherOption || p.ResourceID == otherOption || p.Action == otherOption { diff --git a/coderd/authz/authztest/role.go b/coderd/authz/authztest/role.go index 8a38e77dfa5d5..5ea1f1e2a5809 100644 --- a/coderd/authz/authztest/role.go +++ b/coderd/authz/authztest/role.go @@ -1,11 +1,9 @@ package authztest import ( - . "github.com/coder/coder/coderd/authz" + "github.com/coder/coder/coderd/authz" ) -var _ Permission - // Role can print all possible permutations of the given iterators. type Role struct { // returnSize is how many permissions are the returned set for the role @@ -16,7 +14,7 @@ type Role struct { // This is kinda werird, but the first scan should not move anything. first bool - buffer []*Permission + buffer []*authz.Permission } func NewRole(sets ...Iterable) *Role { @@ -34,7 +32,7 @@ func NewRole(sets ...Iterable) *Role { returnSize: retSize, N: size, PermissionSets: setInterfaces, - buffer: make([]*Permission, retSize), + buffer: make([]*authz.Permission, retSize), } } @@ -60,6 +58,7 @@ func (r *Role) Permissions() Set { } func (r *Role) Each(ea func(set Set)) { + ea(r.Permissions()) for r.Next() { ea(r.Permissions()) } @@ -67,10 +66,6 @@ func (r *Role) Each(ea func(set Set)) { // Next will grab the next cross-product permutation of all permissions of r. func (r *Role) Next() bool { - if !r.first { - r.first = true - return true - } for i := range r.PermissionSets { if r.PermissionSets[i].Next() { break diff --git a/coderd/authz/authztest/set.go b/coderd/authz/authztest/set.go index df8e4c3d7d9d2..44417209f5265 100644 --- a/coderd/authz/authztest/set.go +++ b/coderd/authz/authztest/set.go @@ -3,13 +3,23 @@ package authztest import ( "strings" - . "github.com/coder/coder/coderd/authz" + "github.com/coder/coder/coderd/authz" ) -type Set []*Permission +type Set []*authz.Permission var _ Iterable = (Set)(nil) +func (s Set) Permissions() []authz.Permission { + perms := make([]authz.Permission, 0, len(s)) + for i := range s { + if s[i] != nil { + perms = append(perms, *s[i]) + } + } + return perms +} + func (s Set) Iterator() Iterator { return Union(s) } diff --git a/coderd/authz/role.go b/coderd/authz/role.go new file mode 100644 index 0000000000000..6328268b057d6 --- /dev/null +++ b/coderd/authz/role.go @@ -0,0 +1,6 @@ +package authz + +type Role struct { + Level permLevel + Permissions []Permission +} diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index 1343576c69d3a..59c850e6f44db 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -1,28 +1,37 @@ package authz -// -//// Subject is the actor that is performing the action on an object -//type Subject struct { -// UserID string `json:"user_id"` -// -// SiteRoles []Role `json:"site_roles"` -// -// // Ops are mapped for the resource and the list of operations on the resource for the scope. -// SiteOps []Permission `json:"site_ops"` -// OrgOps []Permission `json:"org_ops"` -// // UserOps only affect objects owned by the user -// UserOps []Permission `json:"user_ops"` -//} -// -//func (s Subject) AllPermissions() []Permission{ -// // Explosion of roles + scopes -// return []Permission{} -//} -// -//// Authn -//type S struct { -// SiteRoles() ([]rbac.Roles, error) -// OrgRoles(ctx context.Context, orgID string) ([]rbac.Roles, error) -// UserRoles() ([]rbac.Roles, error) -// Scopes() ([]rbac.ResourcePermission, error) -//} +import "context" + +type Subject interface { + ID() string + + SiteRoles() ([]Role, error) + OrgRoles(ctx context.Context, orgID string) ([]Role, error) + UserRoles() ([]Role, error) + + //Scopes() ([]Permission, error) +} + +type SimpleSubject struct { + UserID string `json:"user_id"` + + Site []Role `json:"site_roles"` + Org []Role `json:"org_roles"` + User []Role `json:"user_roles"` +} + +func (s SimpleSubject) ID() string { + return s.UserID +} + +func (s SimpleSubject) SiteRoles() ([]Role, error) { + return s.Site, nil +} + +func (s SimpleSubject) OrgRoles() ([]Role, error) { + return s.Org, nil +} + +func (s SimpleSubject) UserRoles() ([]Role, error) { + return s.User, nil +} From a0017e5db6216a3a7119c5d4290620d193c69943 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 31 Mar 2022 14:05:18 -0500 Subject: [PATCH 15/42] Tally up failed tests --- coderd/authz/authz_test.go | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index f13c4166a82d1..4a56650106f97 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/coder/coder/coderd/authz" "github.com/coder/coder/coderd/authz/authztest" + "github.com/stretchr/testify/require" "math/bits" "strings" "testing" @@ -78,22 +79,11 @@ func Test_ExhaustiveAuthorize(t *testing.T) { //}, } - var pvars int - for name, v := range roleVariants { - fmt.Printf("%s: %d\n", name, v.Size()) - pvars += v.Size() - } - var total int = 0 - for _, c := range testCases { - total += len(c.Objs) * pvars - } - fmt.Printf("pvars=%d, total=%d\n", pvars, total) - - var tot int + var failedTests int for _, c := range testCases { t.Run(c.Name, func(t *testing.T) { for _, o := range c.Objs { - for _, v := range roleVariants { + for name, v := range roleVariants { v.Each(func(set authztest.Set) { // TODO: Authz.Permissions does allocations at the moment. We should fix that. err := authz.AuthorizePermissions( @@ -101,14 +91,18 @@ func Test_ExhaustiveAuthorize(t *testing.T) { set.Permissions(), o, authztest.PermAction) - var _ = err - tot++ + if c.Result(name) && err != nil { + failedTests++ + } else if !c.Result(name) && err == nil { + failedTests++ + } }) v.Reset() } } }) } + require.Equal(t, 0, failedTests, fmt.Sprintf("%d tests failed", failedTests)) } func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { From 4b110b3aef7fb2b736849e091453408dd2db2889 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 31 Mar 2022 14:29:25 -0500 Subject: [PATCH 16/42] Change test pkg --- coderd/authz/permission_test.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go index c30f69bbb91aa..2d5811f67b6f4 100644 --- a/coderd/authz/permission_test.go +++ b/coderd/authz/permission_test.go @@ -1,8 +1,10 @@ -package authz +package authz_test import ( - crand "github.com/coder/coder/cryptorand" "testing" + + "github.com/coder/coder/coderd/authz" + crand "github.com/coder/coder/cryptorand" ) func BenchmarkPermissionString(b *testing.B) { @@ -10,7 +12,7 @@ func BenchmarkPermissionString(b *testing.B) { if b.N < total { total = b.N } - perms := make([]Permission, b.N) + perms := make([]authz.Permission, b.N) for n := 0; n < total; n++ { perms[n] = RandomPermission() } @@ -30,13 +32,13 @@ var actions = []string{ "read", "create", "delete", "modify", "*", } -func RandomPermission() Permission { - n, _ := crand.Intn(len(PermissionLevels)) +func RandomPermission() authz.Permission { + n, _ := crand.Intn(len(authz.PermissionLevels)) m, _ := crand.Intn(len(resourceTypes)) a, _ := crand.Intn(len(actions)) - return Permission{ + return authz.Permission{ Sign: n%2 == 0, - Level: PermissionLevels[n], + Level: authz.PermissionLevels[n], ResourceType: resourceTypes[m], ResourceID: "*", Action: actions[a], From 65ef4e3fb4c9455c1522c9035df6c784b9ce9fda Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 31 Mar 2022 16:02:47 -0500 Subject: [PATCH 17/42] Use an interface for the object --- coderd/authz/object.go | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/coderd/authz/object.go b/coderd/authz/object.go index c290161de7074..b6193cee18bd5 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -1,12 +1,43 @@ package authz -// Object is the resource being accessed -type Object struct { - ObjectID string `json:"object_id"` - OwnerID string `json:"owner_id"` - OrgOwnerID string `json:"org_owner_id"` +type Object interface { + ID() string + ResourceType() ResourceType +} + +type UserObject interface { + Object + OwnerID() string +} + +type OrgObject interface { + Object + OrgOwnerID() string +} + +// ZObject is the resource being accessed +type ZObject struct { + ObjectID string `json:"object_id"` + Owner string `json:"owner_id"` + OrgOwner string `json:"org_owner_id"` // ObjectType is "workspace", "project", "devurl", etc ObjectType ResourceType `json:"object_type"` // TODO: SharedUsers? } + +func (z ZObject) ID() string { + return z.ObjectID +} + +func (z ZObject) ResourceType() ResourceType { + return z.ObjectType +} + +func (z ZObject) OwnerID() string { + return z.Owner +} + +func (z ZObject) OrgOwnerID() string { + return z.OrgOwner +} From d2947860f0a4f7e7d86bf2f436a0405e6c204be0 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 1 Apr 2022 11:39:24 +0100 Subject: [PATCH 18/42] fix: make authztest.Objects return correct type --- coderd/authz/authztest/object.go | 6 +++--- coderd/authz/object.go | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/coderd/authz/authztest/object.go b/coderd/authz/authztest/object.go index 21c407e11acbc..190fd99303747 100644 --- a/coderd/authz/authztest/object.go +++ b/coderd/authz/authztest/object.go @@ -5,10 +5,10 @@ import "github.com/coder/coder/coderd/authz" func Objects(pairs ...[]string) []authz.Object { objs := make([]authz.Object, 0, len(pairs)) for _, p := range pairs { - objs = append(objs, authz.Object{ + objs = append(objs, &authz.ZObject{ ObjectID: PermObjectID, - OwnerID: p[0], - OrgOwnerID: p[1], + Owner: p[0], + OrgOwner: p[1], ObjectType: PermObjectType, }) } diff --git a/coderd/authz/object.go b/coderd/authz/object.go index b6193cee18bd5..fe5fc84d1497a 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -26,6 +26,8 @@ type ZObject struct { // TODO: SharedUsers? } +var _ Object = (*ZObject)(nil) + func (z ZObject) ID() string { return z.ObjectID } From c1f8945be896aba838d3f913d3bede422112a1a1 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 1 Apr 2022 11:50:30 +0100 Subject: [PATCH 19/42] refactor: rename consts {Read,Write,Modify,Delete}Action to Action$1 --- coderd/authz/action.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/authz/action.go b/coderd/authz/action.go index 0597b56916d09..709e13b0a0e10 100644 --- a/coderd/authz/action.go +++ b/coderd/authz/action.go @@ -3,8 +3,8 @@ package authz type Action string const ( - ReadAction = "read" - WriteAction = "write" - ModifyAction = "modify" - DeleteAction = "delete" + ActionRead = "read" + ActionWrite = "write" + ActionModify = "modify" + ActionDelete = "delete" ) From 01f3d40924d8838f351bcb4541416b5ceb91ed9d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 1 Apr 2022 11:36:51 -0500 Subject: [PATCH 20/42] chore: Define object interface - Cleanup some code too --- coderd/authz/action.go | 1 + coderd/authz/authz.go | 5 +- coderd/authz/authz_test.go | 2 +- coderd/authz/authztest/group.go | 18 ---- coderd/authz/authztest/group_test.go | 37 --------- coderd/authz/authztest/object.go | 13 ++- coderd/authz/authztest/parser.go | 119 --------------------------- coderd/authz/authztest/set_test.go | 109 ------------------------ coderd/authz/object.go | 48 +++++++---- coderd/authz/resources.go | 31 ++++++- coderd/authz/subject.go | 26 ++++-- 11 files changed, 92 insertions(+), 317 deletions(-) delete mode 100644 coderd/authz/authztest/group_test.go delete mode 100644 coderd/authz/authztest/parser.go delete mode 100644 coderd/authz/authztest/set_test.go diff --git a/coderd/authz/action.go b/coderd/authz/action.go index 0597b56916d09..674b6a5b5ba56 100644 --- a/coderd/authz/action.go +++ b/coderd/authz/action.go @@ -1,5 +1,6 @@ package authz +// Action represents the allowed actions to be done on an object. type Action string const ( diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index 029eb8a831db9..2684a3f62793e 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -1,12 +1,13 @@ package authz // TODO: Implement Authorize -func Authorize(subj Subject, obj Object, action Action) error { +func Authorize(subj Subject, obj Resource, action Action) error { // TODO: Expand subject roles into their permissions as appropriate. Apply scopes. return AuthorizePermissions(subj.ID(), []Permission{}, obj, action) } // AuthorizePermissions runs the authorize function with the raw permissions in a single list. -func AuthorizePermissions(subjID string, permissions []Permission, object Object, action Action) error { +func AuthorizePermissions(subjID string, permissions []Permission, object Resource, action Action) error { + return nil } diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 4a56650106f97..6b58edefa1afc 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -18,7 +18,7 @@ func Test_ExhaustiveAuthorize(t *testing.T) { testCases := []struct { Name string - Objs []authz.Object + Objs []authz.Resource // Action is constant // Subject comes from roleVariants Result func(pv string) bool diff --git a/coderd/authz/authztest/group.go b/coderd/authz/authztest/group.go index 45d770538281b..aa3fede0168ce 100644 --- a/coderd/authz/authztest/group.go +++ b/coderd/authz/authztest/group.go @@ -14,21 +14,3 @@ var ( ) var nilSet = Set{nil} - -// *.*.*.* -//var PermissionSetWPlus = NewRole( -// all.Wildcard().Positive(), -// Union(all.Wildcard().Abstain(), nilSet), -// -// Union(all.Site().Positive(), nilSet), -// Union(all.Site().Negative(), nilSet), -// Union(all.Site().Abstain(), nilSet), -// -// Union(all.AllOrgs().Positive(), nilSet), -// Union(all.AllOrgs().Negative(), nilSet), -// Union(all.AllOrgs().Abstain(), nilSet), -// -// Union(all.User().Positive(), nilSet), -// Union(all.User().Negative(), nilSet), -// Union(all.User().Abstain(), nilSet), -//) diff --git a/coderd/authz/authztest/group_test.go b/coderd/authz/authztest/group_test.go deleted file mode 100644 index a7cd1e94f07cc..0000000000000 --- a/coderd/authz/authztest/group_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package authztest - -import ( - "fmt" - "testing" -) - -func Test_PermissionSetWPlusSearchSpace(t *testing.T) { - all := GroupedPermissions(AllPermissions()) - wplus := NewRole( - all.Wildcard().Positive(), - Union(all.Wildcard().Abstain()[:1], nilSet), - - Union(all.Site().Positive()[:1], nilSet), - Union(all.Site().Negative()[:1], nilSet), - Union(all.Site().Abstain()[:1], nilSet), - - Union(all.AllOrgs().Positive()[:1], nilSet), - Union(all.AllOrgs().Negative()[:1], nilSet), - Union(all.AllOrgs().Abstain()[:1], nilSet), - - Union(all.User().Positive()[:1], nilSet), - Union(all.User().Negative()[:1], nilSet), - Union(all.User().Abstain()[:1], nilSet), - ) - fmt.Println(wplus.N) - fmt.Println(len(AllPermissions())) - for k, v := range all { - fmt.Printf("%s=%d\n", string(k), len(v.All())) - } - - var i int - wplus.Each(func(set Set) { - fmt.Printf("%d: %s\n", i, set.String()) - i++ - }) -} diff --git a/coderd/authz/authztest/object.go b/coderd/authz/authztest/object.go index 21c407e11acbc..ee0ecac34b2c0 100644 --- a/coderd/authz/authztest/object.go +++ b/coderd/authz/authztest/object.go @@ -2,15 +2,12 @@ package authztest import "github.com/coder/coder/coderd/authz" -func Objects(pairs ...[]string) []authz.Object { - objs := make([]authz.Object, 0, len(pairs)) +func Objects(pairs ...[]string) []authz.Resource { + objs := make([]authz.Resource, 0, len(pairs)) for _, p := range pairs { - objs = append(objs, authz.Object{ - ObjectID: PermObjectID, - OwnerID: p[0], - OrgOwnerID: p[1], - ObjectType: PermObjectType, - }) + objs = append(objs, + authz.ResourceType(PermObjectType).Owner(p[0]).Org(p[1]).AsID(PermObjectID), + ) } return objs } diff --git a/coderd/authz/authztest/parser.go b/coderd/authz/authztest/parser.go deleted file mode 100644 index 889fe2b5ce201..0000000000000 --- a/coderd/authz/authztest/parser.go +++ /dev/null @@ -1,119 +0,0 @@ -package authztest - -import ( - "fmt" -) - -type Parser struct { - input string - stack []interface{} - grp SetGroup - - setI []Iterable -} - -func ParseRole(grp SetGroup, input string) *Role { - p := NewParser(grp, input) - p.parse() - return NewRole(p.setI...) -} - -func Parse(grp SetGroup, input string) []Iterable { - p := NewParser(grp, input) - p.parse() - return p.setI -} - -func NewParser(grp SetGroup, input string) *Parser { - - return &Parser{ - grp: grp, - input: input, - stack: make([]interface{}, 0), - } -} - -func (p *Parser) skipSpace(ptr int) int { - for ptr < len(p.input) { - r := p.input[ptr] - switch r { - case ' ', '\t', '\n': - ptr++ - default: - return ptr - } - } - return ptr -} - -func (p *Parser) parse() { - ptr := 0 - for ptr < len(p.input) { - ptr = p.skipSpace(ptr) - r := p.input[ptr] - switch r { - case ' ': - ptr++ - case 'w', 's', 'o', 'm', 'u': - // Time to look ahead for the grp - ptr++ - ptr = p.handleLevel(r, ptr) - default: - panic(fmt.Errorf("cannot handle '%c' at %d", r, ptr)) - } - } -} - -func (p *Parser) handleLevel(l uint8, ptr int) int { - var lg LevelGroup - switch l { - case 'w': - lg = p.grp.Wildcard() - case 's': - lg = p.grp.Site() - case 'o': - lg = p.grp.AllOrgs() - case 'm': - lg = p.grp.OrgMem() - case 'u': - lg = p.grp.User() - } - - // time to look ahead. Find the parenthesis - var sets []Set - var start bool - var stop bool - for { - ptr = p.skipSpace(ptr) - r := p.input[ptr] - if r != '(' && !start { - panic(fmt.Sprintf("Expect a parenthesis at %d", ptr)) - } - switch r { - case '(': - start = true - case ')': - stop = true - case 'p': - sets = append(sets, lg.Positive()) - case 'n': - sets = append(sets, lg.Negative()) - case 'a': - sets = append(sets, lg.Abstain()) - case '*': - sets = append(sets, lg.All()) - case 'e': - // Add the empty perm - sets = append(sets, Set{nil}) - default: - panic(fmt.Errorf("unsupported '%c' for level set", r)) - } - ptr++ - if stop { - p.setI = append(p.setI, Union(sets...)) - return ptr - } - } -} - -//func (p *Parser) diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go deleted file mode 100644 index e4d1d208503a4..0000000000000 --- a/coderd/authz/authztest/set_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package authztest - -import ( - "fmt" - "testing" -) - -func BenchmarkRole(b *testing.B) { - all := GroupedPermissions(AllPermissions()) - r := ParseRole(all, "w(pa) s(*e) s(*e) s(*e) s(pe) s(pe) s(*) s(*)") - b.ResetTimer() - for n := 0; n < b.N; n++ { - if !r.Next() { - r.Reset() - } - FakeAuthorize(r.Permissions()) - } -} - -func TestRole(t *testing.T) { - all := GroupedPermissions(AllPermissions()) - testCases := []struct { - Name string - Permutations *Role - Access bool - }{ // 410,367,658 - { - // [w] x [s1, s2, ""] = (w, s1), (w, s2), (w, "") - Name: "W+", - Permutations: ParseRole(all, "w(p) w(ae) s(pe) s(ne) s(ae) o(pe) o(ne) o(ae) u(pe) u(ne) u(ae)"), - Access: true, - }, - { - Name: "W-", - Permutations: ParseRole(all, "w(n) w(pae) s(*e) o(*e) u(*e)"), - Access: false, - }, - { - Name: "S+", - Permutations: ParseRole(all, "w(a) s(pa) o(*e) u(*e)"), - Access: true, - }, - { - Name: "S-", - Permutations: ParseRole(all, "w(a) s(n) s(pae) o(*e) u(*e)"), - Access: false, - }, - { - Name: "O+", - Permutations: ParseRole(all, "w(a) s(a) o(pa) u(*e)"), - Access: true, - }, - { - Name: "O-", - Permutations: ParseRole(all, "w(a) s(a) o(n) o(pae) u(*e)"), - Access: false, - }, - { - Name: "U+", - Permutations: ParseRole(all, "w(a) s(a) o(a) u(pa)"), - Access: true, - }, - { - Name: "U-", - Permutations: ParseRole(all, "w(a) s(a) o(a) u(n) u(pa)"), - Access: false, - }, - { - Name: "A0", - Permutations: ParseRole(all, "w(a) s(a) o(a) u(a)"), - Access: false, - }, - } - - var total uint64 - for _, c := range testCases { - total += uint64(c.Permutations.N) - } - - for _, c := range testCases { - fmt.Printf("%s: N=%10d, %10f%% of total\n", - c.Name, c.Permutations.N, 100*(float64(c.Permutations.N)/float64(total))) - } - fmt.Printf("Total cases=%d\n", total) - - // This is how you run the test cases - //for _, c := range testCases { - //t.Run(c.Name, func(t *testing.T) { - //c.Permutations.Each(func(set Set) { - // // Actually printing all the errors would be insane - // //require.Equal(t, c.Access, FakeAuthorize(set)) - // FakeAuthorize(set) - //}) - //}) - //} -} - -func FakeAuthorize(s Set) bool { - var f bool - for _, i := range s { - if i == nil { - continue - } - if i.Sign { - return true - } - } - return f -} diff --git a/coderd/authz/object.go b/coderd/authz/object.go index b6193cee18bd5..4dd5a47f70064 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -1,43 +1,63 @@ package authz -type Object interface { +type Resource interface { ID() string ResourceType() ResourceType } type UserObject interface { - Object + Resource OwnerID() string } type OrgObject interface { - Object + Resource OrgOwnerID() string } -// ZObject is the resource being accessed -type ZObject struct { - ObjectID string `json:"object_id"` - Owner string `json:"owner_id"` - OrgOwner string `json:"org_owner_id"` +// zObject is used to create objects for authz checks when you have none in +// hand to run the check on. +// An example is if you want to list all workspaces, you can create a zObject +// that represents the set of workspaces you are trying to get access too. +type zObject struct { + ObjectID string `json:"object_id"` + OwnedBy string `json:"owner_id"` + OwnedByOrg string `json:"org_owner_id"` // ObjectType is "workspace", "project", "devurl", etc ObjectType ResourceType `json:"object_type"` // TODO: SharedUsers? } -func (z ZObject) ID() string { +func (z zObject) ID() string { return z.ObjectID } -func (z ZObject) ResourceType() ResourceType { +func (z zObject) ResourceType() ResourceType { return z.ObjectType } -func (z ZObject) OwnerID() string { - return z.Owner +func (z zObject) OwnerID() string { + return z.OwnedBy } -func (z ZObject) OrgOwnerID() string { - return z.OrgOwner +func (z zObject) OrgOwnerID() string { + return z.OwnedByOrg +} + +// Org adds an org OwnerID to the resource +func (z zObject) Org(orgID string) zObject { + z.OwnedByOrg = orgID + return z +} + +// Owner adds an OwnerID to the resource +func (z zObject) Owner(id string) zObject { + z.OwnedBy = id + return z +} + +func (z zObject) AsID(id string) zObject { + z.ObjectID = id + return z } diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index b183ee221ad92..4d4a1d8d6fc5a 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -1,9 +1,34 @@ package authz +// ResourceType defines the list of available resources for authz. type ResourceType string const ( - ResourceTypeWorkspace = "workspace" - ResourceTypeProject = "project" - ResourceTypeDevURL = "devurl" + ResourceWorkspace ResourceType = "workspace" + ResourceProject ResourceType = "project" + ResourceDevURL ResourceType = "devurl" ) + +func (t ResourceType) ID() string { + return "" +} + +func (t ResourceType) ResourceType() ResourceType { + return t +} + +// Org adds an org OwnerID to the resource +func (r ResourceType) Org(orgID string) zObject { + return zObject{ + OwnedByOrg: orgID, + ObjectType: r, + } +} + +// Owner adds an OwnerID to the resource +func (r ResourceType) Owner(id string) zObject { + return zObject{ + OwnedBy: id, + ObjectType: r, + } +} diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index 59c850e6f44db..6c765687fa04d 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -2,17 +2,27 @@ package authz import "context" +// Subject is the actor that is attempting to do some action on some object or +// set of objects. type Subject interface { + // ID is the ID for the given actor. If it matches the OwnerID ID of the + // object, we can assume the object is owned by this subject. ID() string SiteRoles() ([]Role, error) + // OrgRoles only need to be returned for the organization in question. + // This is because users typically belong to more than 1 organization, + // and grabbing all the roles for all orgs is excessive. OrgRoles(ctx context.Context, orgID string) ([]Role, error) UserRoles() ([]Role, error) - //Scopes() ([]Permission, error) + // Scopes can limit the roles above. + Scopes() ([]Permission, error) } -type SimpleSubject struct { +// SubjectTODO is a placeholder until we get an actual actor struct in place. +// This will come with the Authn epic. +type SubjectTODO struct { UserID string `json:"user_id"` Site []Role `json:"site_roles"` @@ -20,18 +30,22 @@ type SimpleSubject struct { User []Role `json:"user_roles"` } -func (s SimpleSubject) ID() string { +func (s SubjectTODO) ID() string { return s.UserID } -func (s SimpleSubject) SiteRoles() ([]Role, error) { +func (s SubjectTODO) SiteRoles() ([]Role, error) { return s.Site, nil } -func (s SimpleSubject) OrgRoles() ([]Role, error) { +func (s SubjectTODO) OrgRoles() ([]Role, error) { return s.Org, nil } -func (s SimpleSubject) UserRoles() ([]Role, error) { +func (s SubjectTODO) UserRoles() ([]Role, error) { return s.User, nil } + +func (s SubjectTODO) Scopes() ([]Permission, error) { + return []Permission{}, nil +} From de7de6e477abd09bf25d3c39b121c1f8e19f3707 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 1 Apr 2022 12:33:28 -0500 Subject: [PATCH 21/42] test: Unit test extra properties --- coderd/authz/authz_test.go | 30 +++++---- coderd/authz/authztest/group.go | 6 -- coderd/authz/authztest/iterator.go | 79 ---------------------- coderd/authz/authztest/iterator_test.go | 62 +++++++++++++++++ coderd/authz/authztest/level_test.go | 90 +++++++++++++++++++++++++ coderd/authz/authztest/object.go | 13 ---- coderd/authz/authztest/permissions.go | 14 ++-- coderd/authz/object.go | 4 +- coderd/authz/permission.go | 8 +-- coderd/authz/permission_test.go | 4 +- coderd/authz/resources.go | 7 ++ 11 files changed, 190 insertions(+), 127 deletions(-) create mode 100644 coderd/authz/authztest/iterator_test.go create mode 100644 coderd/authz/authztest/level_test.go delete mode 100644 coderd/authz/authztest/object.go diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 6b58edefa1afc..aad8ac79358fc 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -15,6 +15,7 @@ var nilSet = authztest.Set{nil} func Test_ExhaustiveAuthorize(t *testing.T) { all := authztest.GroupedPermissions(authztest.AllPermissions()) roleVariants := permissionVariants(all) + res := authz.ResourceType(authztest.PermObjectType).AsID(authztest.PermObjectID) testCases := []struct { Name string @@ -25,9 +26,9 @@ func Test_ExhaustiveAuthorize(t *testing.T) { }{ { Name: "User:Org", - Objs: authztest.Objects( - []string{authztest.PermMe, authztest.PermOrgID}, - ), + Objs: []authz.Resource{ + res.Owner(authztest.PermMe).Org(authztest.PermOrgID), + }, Result: func(pv string) bool { return strings.Contains(pv, "+") }, @@ -35,10 +36,10 @@ func Test_ExhaustiveAuthorize(t *testing.T) { { // All U+/- tests should fail Name: "NotUser:Org", - Objs: authztest.Objects( - []string{"other", authztest.PermOrgID}, - []string{"", authztest.PermOrgID}, - ), + Objs: []authz.Resource{ + res.Owner("other").Org(authztest.PermOrgID), + res.Owner("").Org(authztest.PermOrgID), + }, Result: func(pv string) bool { if strings.Contains(pv, "U") { return false @@ -49,13 +50,14 @@ func Test_ExhaustiveAuthorize(t *testing.T) { { // All O+/- and U+/- tests should fail Name: "NotUser:NotOrg", - Objs: authztest.Objects( - []string{authztest.PermMe, "non-mem"}, - []string{"other", "non-mem"}, - []string{"other", ""}, - []string{"", "non-mem"}, - []string{"", ""}, - ), + Objs: []authz.Resource{ + res.Owner(authztest.PermMe).Org("non-mem"), + res.Owner("other").Org("non-mem"), + res.Owner("other").Org(""), + res.Owner("").Org("non-mem"), + res.Owner("").Org(""), + }, + Result: func(pv string) bool { if strings.Contains(pv, "U") { return false diff --git a/coderd/authz/authztest/group.go b/coderd/authz/authztest/group.go index aa3fede0168ce..585eca6bc0da8 100644 --- a/coderd/authz/authztest/group.go +++ b/coderd/authz/authztest/group.go @@ -8,9 +8,3 @@ const ( SetNegative PermissionSet = "j!" SetNeutral PermissionSet = "a" ) - -var ( - PermissionSets = []PermissionSet{SetPositive, SetNegative, SetNeutral} -) - -var nilSet = Set{nil} diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/authztest/iterator.go index e58e942d9eb6b..7da7c851d4b0f 100644 --- a/coderd/authz/authztest/iterator.go +++ b/coderd/authz/authztest/iterator.go @@ -79,82 +79,3 @@ func (si *unionIterator) Size() int { func (si *unionIterator) Iterator() Iterator { return si } - -type productI struct { - ReturnSize int - N int - PermissionSets []Iterator - - buffer Set -} - -func ProductI(sets ...Iterable) *productI { - setInterfaces := make([]Iterator, 0, len(sets)) - var retSize int - var size int = 1 - for _, s := range sets { - v := s.Iterator() - setInterfaces = append(setInterfaces, v) - retSize += v.ReturnSize() - // size is the cross product of all Iterator sets - size *= v.Size() - } - return &productI{ - ReturnSize: retSize, - N: size, - PermissionSets: setInterfaces, - buffer: make([]*authz.Permission, retSize), - } -} - -type productIterator struct { - i, j int - a Set - b Set - buffer Set -} - -func Product(a, b Set) *productIterator { - i := &productIterator{ - i: 0, - j: 0, - a: a, - b: b, - } - i.buffer = make(Set, i.ReturnSize()) - return i -} - -func (s *productIterator) Next() bool { - s.i++ - if s.i >= len(s.a) { - s.i = 0 - s.j++ - } - if s.j >= len(s.b) { - return false - } - return true -} - -func (s productIterator) Permissions() Set { - s.buffer[0] = s.a[s.i] - s.buffer[1] = s.b[s.j] - return s.buffer -} - -func (s *productIterator) Reset() { - s.i, s.j = 0, 0 -} - -func (s *productIterator) ReturnSize() int { - return 2 -} - -func (s *productIterator) Size() int { - return len(s.a) * len(s.b) -} - -func (s *productIterator) Iterator() Iterator { - return s -} diff --git a/coderd/authz/authztest/iterator_test.go b/coderd/authz/authztest/iterator_test.go new file mode 100644 index 0000000000000..30833c3c79fbe --- /dev/null +++ b/coderd/authz/authztest/iterator_test.go @@ -0,0 +1,62 @@ +package authztest_test + +import ( + "testing" + + "github.com/coder/coder/coderd/authz" + "github.com/coder/coder/coderd/authz/authztest" + crand "github.com/coder/coder/cryptorand" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +func TestUnion(t *testing.T) { + for i := 0; i < 10; i++ { + allPerms := make(authztest.Set, 0) + // 2 - 4 sets + sets := make([]authztest.Set, 2+must(crand.Intn(2))) + for j := range sets { + sets[j] = make(authztest.Set, 1+must(crand.Intn(4))) + allPerms = append(allPerms, sets[j]...) + } + + u := authztest.Union(sets...) + require.Equal(t, len(allPerms), u.Size(), "union set total") + require.Equal(t, 1, u.ReturnSize(), "union ret size is 1") + for c := 0; ; c++ { + require.Equal(t, allPerms[c], u.Permission(), "permission order") + require.Equal(t, 1, len(u.Permissions()), "permissions size") + require.Equal(t, allPerms[c], u.Permissions()[0], "permission order") + if !u.Next() { + break + } + } + + u.Reset() + require.True(t, u.Next(), "reset should make next true again") + } +} + +func RandomPermission() authz.Permission { + actions := []authz.Action{ + authz.ReadAction, + authz.DeleteAction, + authz.WriteAction, + authz.ModifyAction, + } + return authz.Permission{ + Sign: must(crand.Intn(2))%2 == 0, + Level: authz.PermissionLevels[must(crand.Intn(len(authz.PermissionLevels)))], + LevelID: uuid.New().String(), + ResourceType: authz.ResourceWorkspace, + ResourceID: uuid.New().String(), + Action: actions[must(crand.Intn(len(actions)))], + } +} + +func must[r any](v r, err error) r { + if err != nil { + panic(err) + } + return v +} diff --git a/coderd/authz/authztest/level_test.go b/coderd/authz/authztest/level_test.go new file mode 100644 index 0000000000000..6f0ba250ec8d4 --- /dev/null +++ b/coderd/authz/authztest/level_test.go @@ -0,0 +1,90 @@ +package authztest_test + +import ( + "testing" + + "github.com/coder/coder/coderd/authz" + "github.com/coder/coder/coderd/authz/authztest" + "github.com/stretchr/testify/require" +) + +func Test_GroupedPermissions(t *testing.T) { + set := make(authztest.Set, 0) + var total int + for _, lvl := range authz.PermissionLevels { + for _, s := range []bool{true, false} { + for _, a := range []authz.Action{authz.ReadAction, authztest.OtherOption} { + if lvl == authz.LevelOrg { + set = append(set, &authz.Permission{ + Sign: s, + Level: lvl, + LevelID: "mem", + ResourceType: authz.ResourceWorkspace, + Action: a, + }) + total++ + } + set = append(set, &authz.Permission{ + Sign: s, + Level: lvl, + ResourceType: authz.ResourceWorkspace, + Action: a, + }) + total++ + } + } + } + + require.Equal(t, total, len(set), "total set size") + grp := authztest.GroupedPermissions(set) + grp.Org() + + cases := []struct { + Name string + Lvl authztest.LevelGroup + ExpPos int + ExpNeg int + ExpAbs int + }{ + { + Name: "Wild", + Lvl: grp.Wildcard(), + ExpPos: 1, ExpNeg: 1, ExpAbs: 2, + }, + { + Name: "Site", + Lvl: grp.Site(), + ExpPos: 1, ExpNeg: 1, ExpAbs: 2, + }, + { + Name: "Org", + Lvl: grp.Org(), + ExpPos: 1, ExpNeg: 1, ExpAbs: 2, + }, + { + Name: "Org:mem", + Lvl: grp.OrgMem(), + ExpPos: 1, ExpNeg: 1, ExpAbs: 2, + }, + { + Name: "Org:*", + Lvl: grp.AllOrgs(), + ExpPos: 2, ExpNeg: 2, ExpAbs: 4, + }, + { + Name: "User", + Lvl: grp.User(), + ExpPos: 1, ExpNeg: 1, ExpAbs: 2, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + require.Equal(t, c.ExpPos+c.ExpNeg+c.ExpAbs, len(c.Lvl.All()), "set size") + require.Equal(t, c.ExpPos, len(c.Lvl.Positive()), "correct num pos") + require.Equal(t, c.ExpNeg, len(c.Lvl.Negative()), "correct num neg") + require.Equal(t, c.ExpAbs, len(c.Lvl.Abstain()), "correct num abs") + }) + } + +} diff --git a/coderd/authz/authztest/object.go b/coderd/authz/authztest/object.go deleted file mode 100644 index ee0ecac34b2c0..0000000000000 --- a/coderd/authz/authztest/object.go +++ /dev/null @@ -1,13 +0,0 @@ -package authztest - -import "github.com/coder/coder/coderd/authz" - -func Objects(pairs ...[]string) []authz.Resource { - objs := make([]authz.Resource, 0, len(pairs)) - for _, p := range pairs { - objs = append(objs, - authz.ResourceType(PermObjectType).Owner(p[0]).Org(p[1]).AsID(PermObjectID), - ) - } - return objs -} diff --git a/coderd/authz/authztest/permissions.go b/coderd/authz/authztest/permissions.go index 36abadfeb5eea..6802382a48b34 100644 --- a/coderd/authz/authztest/permissions.go +++ b/coderd/authz/authztest/permissions.go @@ -5,7 +5,7 @@ import ( ) const ( - otherOption = "other" + OtherOption = "other" PermObjectType = "resource" PermAction = "read" PermOrgID = "mem" @@ -15,9 +15,9 @@ const ( var ( levelIDs = []string{"", PermOrgID} - resourceTypes = []string{PermObjectType, "*", otherOption} - resourceIDs = []string{PermObjectID, "*", otherOption} - actions = []string{PermAction, "*", otherOption} + resourceTypes = []authz.ResourceType{PermObjectType, "*", OtherOption} + resourceIDs = []string{PermObjectID, "*", OtherOption} + actions = []authz.Action{PermAction, "*", OtherOption} ) // AllPermissions returns all the possible permissions ever. @@ -57,9 +57,9 @@ func AllPermissions() Set { // Impact returns the impact (positive, negative, abstain) of p func Impact(p *authz.Permission) PermissionSet { - if p.ResourceType == otherOption || - p.ResourceID == otherOption || - p.Action == otherOption { + if p.ResourceType == OtherOption || + p.ResourceID == OtherOption || + p.Action == OtherOption { return SetNeutral } if p.Sign { diff --git a/coderd/authz/object.go b/coderd/authz/object.go index 4dd5a47f70064..ea53f245e6cc4 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -5,12 +5,12 @@ type Resource interface { ResourceType() ResourceType } -type UserObject interface { +type UserResource interface { Resource OwnerID() string } -type OrgObject interface { +type OrgResource interface { Resource OrgOwnerID() string } diff --git a/coderd/authz/permission.go b/coderd/authz/permission.go index 8db492ec92463..96545fdcbe381 100644 --- a/coderd/authz/permission.go +++ b/coderd/authz/permission.go @@ -22,9 +22,9 @@ type Permission struct { // org:1234 LevelID string - ResourceType string + ResourceType ResourceType ResourceID string - Action string + Action Action } // String returns the ... string formatted permission. @@ -44,10 +44,10 @@ func (p Permission) String() string { s.WriteString(p.LevelID) } s.WriteRune('.') - s.WriteString(p.ResourceType) + s.WriteString(string(p.ResourceType)) s.WriteRune('.') s.WriteString(p.ResourceID) s.WriteRune('.') - s.WriteString(p.Action) + s.WriteString(string(p.Action)) return s.String() } diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go index 2d5811f67b6f4..f78dfa013a9fa 100644 --- a/coderd/authz/permission_test.go +++ b/coderd/authz/permission_test.go @@ -23,12 +23,12 @@ func BenchmarkPermissionString(b *testing.B) { } } -var resourceTypes = []string{ +var resourceTypes = []authz.ResourceType{ "project", "config", "user", "user_role", "workspace", "dev-url", "metric", "*", } -var actions = []string{ +var actions = []authz.Action{ "read", "create", "delete", "modify", "*", } diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index 4d4a1d8d6fc5a..e2e070d06d644 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -32,3 +32,10 @@ func (r ResourceType) Owner(id string) zObject { ObjectType: r, } } + +func (r ResourceType) AsID(id string) zObject { + return zObject{ + ObjectID: id, + ObjectType: r, + } +} From 30c6568e8571f21cf049a939225339d2cdee8eb2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 1 Apr 2022 12:45:28 -0500 Subject: [PATCH 22/42] put back interface assertion --- coderd/authz/authztest/object.go | 16 ---------------- coderd/authz/object.go | 4 ++++ 2 files changed, 4 insertions(+), 16 deletions(-) delete mode 100644 coderd/authz/authztest/object.go diff --git a/coderd/authz/authztest/object.go b/coderd/authz/authztest/object.go deleted file mode 100644 index 190fd99303747..0000000000000 --- a/coderd/authz/authztest/object.go +++ /dev/null @@ -1,16 +0,0 @@ -package authztest - -import "github.com/coder/coder/coderd/authz" - -func Objects(pairs ...[]string) []authz.Object { - objs := make([]authz.Object, 0, len(pairs)) - for _, p := range pairs { - objs = append(objs, &authz.ZObject{ - ObjectID: PermObjectID, - Owner: p[0], - OrgOwner: p[1], - ObjectType: PermObjectType, - }) - } - return objs -} diff --git a/coderd/authz/object.go b/coderd/authz/object.go index ea53f245e6cc4..c92450c766854 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -15,6 +15,10 @@ type OrgResource interface { OrgOwnerID() string } +var _ Resource = (*zObject)(nil) +var _ UserResource = (*zObject)(nil) +var _ OrgResource = (*zObject)(nil) + // zObject is used to create objects for authz checks when you have none in // hand to run the check on. // An example is if you want to list all workspaces, you can create a zObject From a419a6520460f94edc5bf813ea9b1155b24bd069 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 1 Apr 2022 12:46:33 -0500 Subject: [PATCH 23/42] Fix some compile errors from merge --- coderd/authz/authztest/iterator_test.go | 10 +++++----- coderd/authz/authztest/level_test.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/coderd/authz/authztest/iterator_test.go b/coderd/authz/authztest/iterator_test.go index 30833c3c79fbe..1a9026b7677a5 100644 --- a/coderd/authz/authztest/iterator_test.go +++ b/coderd/authz/authztest/iterator_test.go @@ -2,7 +2,7 @@ package authztest_test import ( "testing" - + "github.com/coder/coder/coderd/authz" "github.com/coder/coder/coderd/authz/authztest" crand "github.com/coder/coder/cryptorand" @@ -39,10 +39,10 @@ func TestUnion(t *testing.T) { func RandomPermission() authz.Permission { actions := []authz.Action{ - authz.ReadAction, - authz.DeleteAction, - authz.WriteAction, - authz.ModifyAction, + authz.ActionRead, + authz.ActionWrite, + authz.ActionModify, + authz.ActionDelete, } return authz.Permission{ Sign: must(crand.Intn(2))%2 == 0, diff --git a/coderd/authz/authztest/level_test.go b/coderd/authz/authztest/level_test.go index 6f0ba250ec8d4..356ebcecd2fc5 100644 --- a/coderd/authz/authztest/level_test.go +++ b/coderd/authz/authztest/level_test.go @@ -13,7 +13,7 @@ func Test_GroupedPermissions(t *testing.T) { var total int for _, lvl := range authz.PermissionLevels { for _, s := range []bool{true, false} { - for _, a := range []authz.Action{authz.ReadAction, authztest.OtherOption} { + for _, a := range []authz.Action{authz.ActionRead, authztest.OtherOption} { if lvl == authz.LevelOrg { set = append(set, &authz.Permission{ Sign: s, From bbd1c4c05703322672bab86a1eebf0f65f54c61e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 1 Apr 2022 14:20:06 -0500 Subject: [PATCH 24/42] test: Roles, sets, permissions, iterators --- coderd/authz/authztest/iterator_test.go | 35 ++++++++---- coderd/authz/authztest/permissions_test.go | 15 +++++ coderd/authz/authztest/role.go | 1 + coderd/authz/authztest/role_test.go | 59 ++++++++++++++++++++ coderd/authz/authztest/set.go | 2 + coderd/authz/authztest/set_test.go | 64 ++++++++++++++++++++++ 6 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 coderd/authz/authztest/permissions_test.go create mode 100644 coderd/authz/authztest/role_test.go create mode 100644 coderd/authz/authztest/set_test.go diff --git a/coderd/authz/authztest/iterator_test.go b/coderd/authz/authztest/iterator_test.go index 1a9026b7677a5..26050fa8f9b79 100644 --- a/coderd/authz/authztest/iterator_test.go +++ b/coderd/authz/authztest/iterator_test.go @@ -11,30 +11,41 @@ import ( ) func TestUnion(t *testing.T) { - for i := 0; i < 10; i++ { + for i := 0; i < 100; i++ { allPerms := make(authztest.Set, 0) // 2 - 4 sets - sets := make([]authztest.Set, 2+must(crand.Intn(2))) + sets := make([]authztest.Set, 1+must(crand.Intn(2))) for j := range sets { - sets[j] = make(authztest.Set, 1+must(crand.Intn(4))) + sets[j] = RandomSet(1 + must(crand.Intn(4))) allPerms = append(allPerms, sets[j]...) } - u := authztest.Union(sets...) - require.Equal(t, len(allPerms), u.Size(), "union set total") - require.Equal(t, 1, u.ReturnSize(), "union ret size is 1") + ui := authztest.Union(sets...).Iterator() + require.Equal(t, len(allPerms), ui.Size(), "union set total") + require.Equal(t, 1, ui.ReturnSize(), "union ret size is 1") for c := 0; ; c++ { - require.Equal(t, allPerms[c], u.Permission(), "permission order") - require.Equal(t, 1, len(u.Permissions()), "permissions size") - require.Equal(t, allPerms[c], u.Permissions()[0], "permission order") - if !u.Next() { + require.Equal(t, 1, len(ui.Permissions()), "permissions size") + require.Equal(t, allPerms[c], ui.Permissions()[0], "permission order") + if !ui.Next() { break } } - u.Reset() - require.True(t, u.Next(), "reset should make next true again") + ui.Reset() + // If the size is 1, next will always return false + if ui.Size() > 1 { + require.True(t, ui.Next(), "reset should make next true again") + } + } +} + +func RandomSet(size int) authztest.Set { + set := make(authztest.Set, 0, size) + for i := 0; i < size; i++ { + p := RandomPermission() + set = append(set, &p) } + return set } func RandomPermission() authz.Permission { diff --git a/coderd/authz/authztest/permissions_test.go b/coderd/authz/authztest/permissions_test.go new file mode 100644 index 0000000000000..99c322cc98dda --- /dev/null +++ b/coderd/authz/authztest/permissions_test.go @@ -0,0 +1,15 @@ +package authztest_test + +import ( + "testing" + + "github.com/coder/coder/coderd/authz/authztest" + "github.com/stretchr/testify/require" +) + +func Test_AllPermissions(t *testing.T) { + // If this changes, then we might have to fix some other tests. This constant + // is the basis for understanding the permutation counts. + const totalUniquePermissions int = 270 + require.Equal(t, len(authztest.AllPermissions()), totalUniquePermissions, "expected set size") +} diff --git a/coderd/authz/authztest/role.go b/coderd/authz/authztest/role.go index 5ea1f1e2a5809..085d595bd963d 100644 --- a/coderd/authz/authztest/role.go +++ b/coderd/authz/authztest/role.go @@ -65,6 +65,7 @@ func (r *Role) Each(ea func(set Set)) { } // Next will grab the next cross-product permutation of all permissions of r. +// When Next() returns false, the role would be Reset() func (r *Role) Next() bool { for i := range r.PermissionSets { if r.PermissionSets[i].Next() { diff --git a/coderd/authz/authztest/role_test.go b/coderd/authz/authztest/role_test.go new file mode 100644 index 0000000000000..52550e912abe5 --- /dev/null +++ b/coderd/authz/authztest/role_test.go @@ -0,0 +1,59 @@ +package authztest_test + +import ( + "testing" + + "github.com/coder/coder/coderd/authz/authztest" + crand "github.com/coder/coder/cryptorand" + "github.com/stretchr/testify/require" +) + +func Test_NewRole(t *testing.T) { + for i := 0; i < 50; i++ { + sets := make([]authztest.Iterable, 1+(i%4)) + var total int = 1 + for j := range sets { + size := 1 + must(crand.Intn(3)) + if i < 5 { + // Enforce 1 size sets for some cases + size = 1 + } + sets[j] = RandomSet(size) + total *= size + } + + crossProduct := authztest.NewRole(sets...) + t.Run("CrossProduct", func(t *testing.T) { + require.Equal(t, total, crossProduct.Size(), "correct N") + require.Equal(t, len(sets), crossProduct.ReturnSize(), "return size") + var c int + crossProduct.Each(func(set authztest.Set) { + require.Equal(t, crossProduct.ReturnSize(), len(set), "each set is correct size") + c++ + }) + require.Equal(t, total, c, "each run N times") + + if crossProduct.Size() > 1 { + crossProduct.Reset() + require.Truef(t, crossProduct.Next(), "reset should always make this true") + } + }) + + t.Run("NestedRoles", func(t *testing.T) { + merged := authztest.NewRole(sets[0]) + for i := 1; i < len(sets); i++ { + merged = authztest.NewRole(sets[i], merged) + } + + crossProduct.Reset() + for { + require.Equal(t, crossProduct.Permissions(), merged.Permissions(), "same next") + mn, cn := merged.Next(), crossProduct.Next() + require.Equal(t, cn, mn, "next should be same") + if !cn { + break + } + } + }) + } +} diff --git a/coderd/authz/authztest/set.go b/coderd/authz/authztest/set.go index 44417209f5265..de11f6e581ead 100644 --- a/coderd/authz/authztest/set.go +++ b/coderd/authz/authztest/set.go @@ -10,6 +10,8 @@ type Set []*authz.Permission var _ Iterable = (Set)(nil) +// Permissions is a helper function to get the Permissions as non-pointers. +// permissions are omitted func (s Set) Permissions() []authz.Permission { perms := make([]authz.Permission, 0, len(s)) for i := range s { diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go new file mode 100644 index 0000000000000..c861e3e77bb16 --- /dev/null +++ b/coderd/authz/authztest/set_test.go @@ -0,0 +1,64 @@ +package authztest_test + +import ( + "github.com/coder/coder/coderd/authz" + "github.com/coder/coder/coderd/authz/authztest" + "testing" + + crand "github.com/coder/coder/cryptorand" + "github.com/stretchr/testify/require" +) + +func Test_Set(t *testing.T) { + t.Run("Simple", func(t *testing.T) { + for i := 0; i < 10; i++ { + set := RandomSet(i) + require.Equal(t, i, len(set), "set size") + require.Equal(t, i, len(set.Permissions()), "set size") + perms := set.Permissions() + for i, p := range set { + require.Equal(t, *p, perms[i]) + } + } + }) + + t.Run("NilPerms", func(t *testing.T) { + for i := 0; i < 100; i++ { + set := RandomSet(i) + // Set some nils + nilCount := 0 + for i := 0; i < len(set); i++ { + if must(crand.Bool()) { + set[i] = nil + nilCount++ + } + } + require.Equal(t, i-nilCount, len(set.Permissions())) + } + }) + + t.Run("String", func(t *testing.T) { + set := authztest.Set{ + &authz.Permission{ + Sign: true, + Level: authz.LevelOrg, + LevelID: "1234", + ResourceType: authz.ResourceWorkspace, + ResourceID: "1234", + Action: authz.ActionRead, + }, + &authz.Permission{ + Sign: false, + Level: authz.LevelSite, + LevelID: "", + ResourceType: authz.ResourceWorkspace, + ResourceID: "*", + Action: authz.ActionRead, + }, + } + + require.Equal(t, + "+org:1234.workspace.1234.read, -site.workspace.*.read", + set.String(), "exp string") + }) +} From def010fe2bace1b4d243a9844e759ac0d4e3e669 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 1 Apr 2022 14:29:03 -0500 Subject: [PATCH 25/42] Test string functions --- coderd/authz/authz_test.go | 5 ++--- coderd/authz/authztest/role_test.go | 16 +++++++--------- coderd/authz/authztest/set_test.go | 1 + 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index aad8ac79358fc..cb3863fdc3ac6 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -1,10 +1,8 @@ package authz_test import ( - "fmt" "github.com/coder/coder/coderd/authz" "github.com/coder/coder/coderd/authz/authztest" - "github.com/stretchr/testify/require" "math/bits" "strings" "testing" @@ -104,7 +102,8 @@ func Test_ExhaustiveAuthorize(t *testing.T) { } }) } - require.Equal(t, 0, failedTests, fmt.Sprintf("%d tests failed", failedTests)) + // TODO: @emyrk when we implement the correct authorize, we can enable this check. + //require.Equal(t, 0, failedTests, fmt.Sprintf("%d tests failed", failedTests)) } func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { diff --git a/coderd/authz/authztest/role_test.go b/coderd/authz/authztest/role_test.go index 52550e912abe5..3d884e4b86415 100644 --- a/coderd/authz/authztest/role_test.go +++ b/coderd/authz/authztest/role_test.go @@ -45,15 +45,13 @@ func Test_NewRole(t *testing.T) { merged = authztest.NewRole(sets[i], merged) } - crossProduct.Reset() - for { - require.Equal(t, crossProduct.Permissions(), merged.Permissions(), "same next") - mn, cn := merged.Next(), crossProduct.Next() - require.Equal(t, cn, mn, "next should be same") - if !cn { - break - } - } + require.Equal(t, crossProduct.Size(), merged.Size()) + var c int + merged.Each(func(set authztest.Set) { + require.Equal(t, merged.ReturnSize(), len(set), "each set is correct size") + c++ + }) + require.Equal(t, merged.Size(), c, "each run N times") }) } } diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go index c861e3e77bb16..f13aa079bec46 100644 --- a/coderd/authz/authztest/set_test.go +++ b/coderd/authz/authztest/set_test.go @@ -47,6 +47,7 @@ func Test_Set(t *testing.T) { ResourceID: "1234", Action: authz.ActionRead, }, + nil, &authz.Permission{ Sign: false, Level: authz.LevelSite, From c4ee59062c16df00df31b63143ba69593fd92283 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 4 Apr 2022 09:46:51 -0500 Subject: [PATCH 26/42] test: Unit test permission string - Add some docs --- coderd/authz/authz_test.go | 5 +-- coderd/authz/authztest/README.md | 23 +++++++++++++ coderd/authz/authztest/doc.go | 2 ++ coderd/authz/authztest/iterator.go | 2 +- coderd/authz/authztest/role.go | 5 +-- coderd/authz/authztest/set_test.go | 4 +-- coderd/authz/object.go | 1 + coderd/authz/permission_test.go | 53 ++++++++++++++++++++++++++++++ coderd/authz/subject.go | 1 + 9 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 coderd/authz/authztest/README.md create mode 100644 coderd/authz/authztest/doc.go diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index cb3863fdc3ac6..3aceb8fed8c62 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -1,11 +1,12 @@ package authz_test import ( - "github.com/coder/coder/coderd/authz" - "github.com/coder/coder/coderd/authz/authztest" "math/bits" "strings" "testing" + + "github.com/coder/coder/coderd/authz" + "github.com/coder/coder/coderd/authz/authztest" ) var nilSet = authztest.Set{nil} diff --git a/coderd/authz/authztest/README.md b/coderd/authz/authztest/README.md new file mode 100644 index 0000000000000..619f9b9c88e0d --- /dev/null +++ b/coderd/authz/authztest/README.md @@ -0,0 +1,23 @@ +# Authztest + +An authz permission is a combination of `level`, `resource_type`, `resource_id`, and action`. For testing purposes, we can assume only 1 action and resource exists. This package can generate all possible permissions from this. + +A `Set` is a slice of permissions. The search space of all possible sets is too large, so instead this package allows generating more meaningful sets for testing. This is equivalent to pruning in AI problems: a technique to reduce the size of the search space by removing parts that do not have significance. + +This is the final pruned search space used in authz. Each set is represented by a ✅, ❌, or ⛶. The leftmost set in a row that is not '⛶' is the impactful set. The impactful set determines the access result. All other sets are non-impactful, and should include the `` permission. The resulting search space for a row is the cross product between all sets in said row. + +| Row | * | Site | Org | Org:mem | User | Access | +|-----|------|------|------|---------|------|--------| +| W+ | ✅⛶ | ✅❌⛶ | ✅❌⛶ | ✅❌⛶ | ✅❌⛶ | ✅ | +| W- | ❌+✅⛶ | ✅❌⛶ | ✅❌⛶ | ✅❌⛶ | ✅❌⛶ | ❌ | +| S+ | ⛶ | ✅⛶ | ✅❌⛶ | ❌✅⛶ | ❌✅⛶ | ✅ | +| S- | ⛶ | ❌+✅⛶ | ✅❌⛶ | ❌✅⛶ | ❌✅⛶ | ❌ | +| O+ | ⛶ | ⛶ | ✅⛶ | ❌✅⛶ | ❌✅⛶ | ✅ | +| O- | ⛶ | ⛶ | ❌+✅⛶ | ❌✅⛶ | ❌✅⛶ | ❌ | +| M+ | ⛶ | ⛶ | ⛶ | ✅⛶ | ❌✅⛶ | ✅ | +| M- | ⛶ | ⛶ | ⛶ | ❌+✅⛶ | ❌✅⛶ | ❌ | +| U+ | ⛶ | ⛶ | ⛶ | ⛶ | ✅⛶ | ✅ | +| U- | ⛶ | ⛶ | ⛶ | ⛶ | ❌+✅⛶ | ❌ | +| A+ | ⛶ | ⛶ | ⛶ | ⛶ | ✅+⛶ | ✅ | +| A- | ⛶ | ⛶ | ⛶ | ⛶ | ⛶ | ❌ | + diff --git a/coderd/authz/authztest/doc.go b/coderd/authz/authztest/doc.go new file mode 100644 index 0000000000000..3acc3fcdc1a8c --- /dev/null +++ b/coderd/authz/authztest/doc.go @@ -0,0 +1,2 @@ +// Package authztest is a helper package for generating permissions to test the authz library. +package authztest diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/authztest/iterator.go index 7da7c851d4b0f..cef2b1956ace1 100644 --- a/coderd/authz/authztest/iterator.go +++ b/coderd/authz/authztest/iterator.go @@ -18,7 +18,7 @@ type Iterator interface { Size() int } -// unionIterator is very primitive, just used to hold a place in a set. +// unionIterator is used to merge sets, or a union in set theory. type unionIterator struct { // setIdx determines which set the offset is for setIdx int diff --git a/coderd/authz/authztest/role.go b/coderd/authz/authztest/role.go index 085d595bd963d..4a8cf14cb7e1e 100644 --- a/coderd/authz/authztest/role.go +++ b/coderd/authz/authztest/role.go @@ -4,14 +4,15 @@ import ( "github.com/coder/coder/coderd/authz" ) -// Role can print all possible permutations of the given iterators. +// Role can print all possible permutations of the given iterators. It represents +// the cross product between all sets given. type Role struct { // returnSize is how many permissions are the returned set for the role returnSize int // N is the total number of permutations of sets this role will produce. N int PermissionSets []Iterator - // This is kinda werird, but the first scan should not move anything. + // This is kinda weird, but the first scan should not move anything. first bool buffer []*authz.Permission diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go index f13aa079bec46..5c389e593152f 100644 --- a/coderd/authz/authztest/set_test.go +++ b/coderd/authz/authztest/set_test.go @@ -1,10 +1,10 @@ package authztest_test import ( - "github.com/coder/coder/coderd/authz" - "github.com/coder/coder/coderd/authz/authztest" "testing" + "github.com/coder/coder/coderd/authz" + "github.com/coder/coder/coderd/authz/authztest" crand "github.com/coder/coder/cryptorand" "github.com/stretchr/testify/require" ) diff --git a/coderd/authz/object.go b/coderd/authz/object.go index c92450c766854..d3b9486b8f12a 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -23,6 +23,7 @@ var _ OrgResource = (*zObject)(nil) // hand to run the check on. // An example is if you want to list all workspaces, you can create a zObject // that represents the set of workspaces you are trying to get access too. +// Do not export this type, as it can be created from a resource type constant. type zObject struct { ObjectID string `json:"object_id"` OwnedBy string `json:"owner_id"` diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go index f78dfa013a9fa..1f7c8ae62d11f 100644 --- a/coderd/authz/permission_test.go +++ b/coderd/authz/permission_test.go @@ -1,12 +1,65 @@ package authz_test import ( + "github.com/stretchr/testify/require" "testing" "github.com/coder/coder/coderd/authz" crand "github.com/coder/coder/cryptorand" ) +func Test_PermissionString(t *testing.T) { + testCases := []struct { + Name string + Permission authz.Permission + Expected string + }{ + { + Name: "BasicPositive", + Permission: authz.Permission{ + Sign: true, + Level: authz.LevelSite, + LevelID: "", + ResourceType: authz.ResourceWorkspace, + ResourceID: "*", + Action: authz.ActionRead, + }, + Expected: "+site.workspace.*.read", + }, + { + Name: "BasicNegative", + Permission: authz.Permission{ + Sign: false, + Level: authz.LevelUser, + LevelID: "", + ResourceType: authz.ResourceDevURL, + ResourceID: "1234", + Action: authz.ActionWrite, + }, + Expected: "-user.devurl.1234.write", + }, + { + Name: "OrgID", + Permission: authz.Permission{ + Sign: false, + Level: authz.LevelOrg, + LevelID: "default", + ResourceType: authz.ResourceProject, + ResourceID: "456", + Action: authz.ActionModify, + }, + Expected: "-org:default.project.456.modify", + }, + } + + for _, c := range testCases { + t.Run(c.Name, func(t *testing.T) { + require.Equal(t, c.Expected, c.Permission.String()) + }) + } + +} + func BenchmarkPermissionString(b *testing.B) { total := 10000 if b.N < total { diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index 6c765687fa04d..dde0bf0275a1a 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -22,6 +22,7 @@ type Subject interface { // SubjectTODO is a placeholder until we get an actual actor struct in place. // This will come with the Authn epic. +// TODO: @emyrk delete this data structure when authn exists type SubjectTODO struct { UserID string `json:"user_id"` From 84e3ab9ed21fac9a6e269a700e558ac3b5147be7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 4 Apr 2022 10:09:30 -0500 Subject: [PATCH 27/42] Add A+ and A- --- coderd/authz/authz_test.go | 21 ++++++++++++++++++++- coderd/authz/permission_test.go | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 3aceb8fed8c62..4852a467f97a0 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -167,7 +167,26 @@ func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { noise(an, all.Wildcard(), all.Site(), all.Org()), neg(all.User()), ), - // TODO: @Emyrk the abstain sets + // Abstain + "A+": authztest.NewRole( + authztest.Union( + all.Wildcard().Abstain(), + all.Site().Abstain(), + all.Org().Abstain(), + all.OrgMem().Abstain(), + all.User().Abstain(), + ), + all.User().Positive()[:1], + ), + "A-": authztest.NewRole( + authztest.Union( + all.Wildcard().Abstain(), + all.Site().Abstain(), + all.Org().Abstain(), + all.OrgMem().Abstain(), + all.User().Abstain(), + ), + ), } } diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go index 1f7c8ae62d11f..7d5678a1ad3d4 100644 --- a/coderd/authz/permission_test.go +++ b/coderd/authz/permission_test.go @@ -1,11 +1,11 @@ package authz_test import ( - "github.com/stretchr/testify/require" "testing" "github.com/coder/coder/coderd/authz" crand "github.com/coder/coder/cryptorand" + "github.com/stretchr/testify/require" ) func Test_PermissionString(t *testing.T) { From c2eec183e03c9de992695245bb0267e63d6329b2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 4 Apr 2022 10:12:46 -0500 Subject: [PATCH 28/42] Parallelize tests --- coderd/authz/authz_test.go | 2 ++ coderd/authz/authztest/iterator_test.go | 2 ++ coderd/authz/authztest/level_test.go | 3 +- coderd/authz/authztest/permissions_test.go | 2 ++ coderd/authz/authztest/set_test.go | 8 +++++ coderd/authz/permission_test.go | 41 ++-------------------- 6 files changed, 18 insertions(+), 40 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 4852a467f97a0..c84934f728a51 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -12,6 +12,8 @@ import ( var nilSet = authztest.Set{nil} func Test_ExhaustiveAuthorize(t *testing.T) { + t.Parallel() + all := authztest.GroupedPermissions(authztest.AllPermissions()) roleVariants := permissionVariants(all) res := authz.ResourceType(authztest.PermObjectType).AsID(authztest.PermObjectID) diff --git a/coderd/authz/authztest/iterator_test.go b/coderd/authz/authztest/iterator_test.go index 26050fa8f9b79..69ac061a7bff6 100644 --- a/coderd/authz/authztest/iterator_test.go +++ b/coderd/authz/authztest/iterator_test.go @@ -11,6 +11,8 @@ import ( ) func TestUnion(t *testing.T) { + t.Parallel() + for i := 0; i < 100; i++ { allPerms := make(authztest.Set, 0) // 2 - 4 sets diff --git a/coderd/authz/authztest/level_test.go b/coderd/authz/authztest/level_test.go index 356ebcecd2fc5..5c3b810a5f0d5 100644 --- a/coderd/authz/authztest/level_test.go +++ b/coderd/authz/authztest/level_test.go @@ -9,6 +9,8 @@ import ( ) func Test_GroupedPermissions(t *testing.T) { + t.Parallel() + set := make(authztest.Set, 0) var total int for _, lvl := range authz.PermissionLevels { @@ -86,5 +88,4 @@ func Test_GroupedPermissions(t *testing.T) { require.Equal(t, c.ExpAbs, len(c.Lvl.Abstain()), "correct num abs") }) } - } diff --git a/coderd/authz/authztest/permissions_test.go b/coderd/authz/authztest/permissions_test.go index 99c322cc98dda..f169e040fd285 100644 --- a/coderd/authz/authztest/permissions_test.go +++ b/coderd/authz/authztest/permissions_test.go @@ -8,6 +8,8 @@ import ( ) func Test_AllPermissions(t *testing.T) { + t.Parallel() + // If this changes, then we might have to fix some other tests. This constant // is the basis for understanding the permutation counts. const totalUniquePermissions int = 270 diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go index 5c389e593152f..e922f573d5b83 100644 --- a/coderd/authz/authztest/set_test.go +++ b/coderd/authz/authztest/set_test.go @@ -10,7 +10,11 @@ import ( ) func Test_Set(t *testing.T) { + t.Parallel() + t.Run("Simple", func(t *testing.T) { + t.Parallel() + for i := 0; i < 10; i++ { set := RandomSet(i) require.Equal(t, i, len(set), "set size") @@ -23,6 +27,8 @@ func Test_Set(t *testing.T) { }) t.Run("NilPerms", func(t *testing.T) { + t.Parallel() + for i := 0; i < 100; i++ { set := RandomSet(i) // Set some nils @@ -38,6 +44,8 @@ func Test_Set(t *testing.T) { }) t.Run("String", func(t *testing.T) { + t.Parallel() + set := authztest.Set{ &authz.Permission{ Sign: true, diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go index 7d5678a1ad3d4..bac2fec538737 100644 --- a/coderd/authz/permission_test.go +++ b/coderd/authz/permission_test.go @@ -4,11 +4,12 @@ import ( "testing" "github.com/coder/coder/coderd/authz" - crand "github.com/coder/coder/cryptorand" "github.com/stretchr/testify/require" ) func Test_PermissionString(t *testing.T) { + t.Parallel() + testCases := []struct { Name string Permission authz.Permission @@ -59,41 +60,3 @@ func Test_PermissionString(t *testing.T) { } } - -func BenchmarkPermissionString(b *testing.B) { - total := 10000 - if b.N < total { - total = b.N - } - perms := make([]authz.Permission, b.N) - for n := 0; n < total; n++ { - perms[n] = RandomPermission() - } - - b.ResetTimer() - for n := 0; n < b.N; n++ { - var _ = perms[n%total].String() - } -} - -var resourceTypes = []authz.ResourceType{ - "project", "config", "user", "user_role", - "workspace", "dev-url", "metric", "*", -} - -var actions = []authz.Action{ - "read", "create", "delete", "modify", "*", -} - -func RandomPermission() authz.Permission { - n, _ := crand.Intn(len(authz.PermissionLevels)) - m, _ := crand.Intn(len(resourceTypes)) - a, _ := crand.Intn(len(actions)) - return authz.Permission{ - Sign: n%2 == 0, - Level: authz.PermissionLevels[n], - ResourceType: resourceTypes[m], - ResourceID: "*", - Action: actions[a], - } -} From 5a2834a1ee4894bedea5087cbbf6e396f98b1bbd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 4 Apr 2022 10:40:55 -0500 Subject: [PATCH 29/42] fix code line in readme --- coderd/authz/authztest/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/authz/authztest/README.md b/coderd/authz/authztest/README.md index 619f9b9c88e0d..09edb317a4d02 100644 --- a/coderd/authz/authztest/README.md +++ b/coderd/authz/authztest/README.md @@ -1,6 +1,6 @@ # Authztest -An authz permission is a combination of `level`, `resource_type`, `resource_id`, and action`. For testing purposes, we can assume only 1 action and resource exists. This package can generate all possible permissions from this. +An authz permission is a combination of `level`, `resource_type`, `resource_id`, and `action`. For testing purposes, we can assume only 1 action and resource exists. This package can generate all possible permissions from this. A `Set` is a slice of permissions. The search space of all possible sets is too large, so instead this package allows generating more meaningful sets for testing. This is equivalent to pruning in AI problems: a technique to reduce the size of the search space by removing parts that do not have significance. From 2804b920e82f141849412b636b5d4bcce7d15ae6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 4 Apr 2022 18:02:54 -0500 Subject: [PATCH 30/42] test: ParsePermissions from strings Add some examples --- coderd/authz/action.go | 2 +- coderd/authz/authztest/iterator_test.go | 2 +- coderd/authz/example_test.go | 67 ++++++++++++++++++++ coderd/authz/permission.go | 69 ++++++++++++++++++++- coderd/authz/permission_test.go | 82 ++++++++++++++++++++++++- coderd/authz/resources.go | 1 + coderd/authz/role.go | 1 - coderd/authz/subject.go | 27 ++++++-- 8 files changed, 240 insertions(+), 11 deletions(-) create mode 100644 coderd/authz/example_test.go diff --git a/coderd/authz/action.go b/coderd/authz/action.go index bcf4685ec64fb..f1469dbe71d19 100644 --- a/coderd/authz/action.go +++ b/coderd/authz/action.go @@ -5,7 +5,7 @@ type Action string const ( ActionRead = "read" - ActionWrite = "write" + ActionCreate = "create" ActionModify = "modify" ActionDelete = "delete" ) diff --git a/coderd/authz/authztest/iterator_test.go b/coderd/authz/authztest/iterator_test.go index 69ac061a7bff6..4182c423b6a7d 100644 --- a/coderd/authz/authztest/iterator_test.go +++ b/coderd/authz/authztest/iterator_test.go @@ -53,7 +53,7 @@ func RandomSet(size int) authztest.Set { func RandomPermission() authz.Permission { actions := []authz.Action{ authz.ActionRead, - authz.ActionWrite, + authz.ActionCreate, authz.ActionModify, authz.ActionDelete, } diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go new file mode 100644 index 0000000000000..04427cf30c63e --- /dev/null +++ b/coderd/authz/example_test.go @@ -0,0 +1,67 @@ +package authz_test + +import ( + "testing" + + "github.com/coder/coder/coderd/authz" + "github.com/stretchr/testify/require" +) + +// Test_Example gives some examples on how to use the authz library. +// This serves to test syntax more than functionality. +func Test_Example(t *testing.T) { + t.Parallel() + + // user will become an authn object, and can even be a database.User if it + // fulfills the interface. Until then, use a placeholder. + user := authz.SubjectTODO{ + UserID: "alice", + // No site perms + Site: []authz.Role{}, + Org: map[string][]authz.Role{ + // Admin of org "default". + "default": {{Permissions: must(authz.ParsePermissions("+org.*.*.*"))}}, + }, + User: []authz.Role{ + // Site user role + {Permissions: must(authz.ParsePermissions("+user.*.*.*"))}, + }, + } + + // TODO: Uncomment all assertions when implementation is done. + + t.Run("ReadAllWorkspaces", func(t *testing.T) { + // To read all workspaces on the site + err := authz.Authorize(user, authz.ResourceWorkspace, authz.ActionRead) + var _ = err + //require.Error(t, err, "this user cannot read all workspaces") + }) + + t.Run("ReadOrgWorkspaces", func(t *testing.T) { + // To read all workspaces on the org 'default' + err := authz.Authorize(user, authz.ResourceWorkspace.Org("default"), authz.ActionRead) + require.NoError(t, err, "this user can read all org workspaces in 'default'") + }) + + t.Run("ReadMyWorkspace", func(t *testing.T) { + // Note 'database.Workspace' could fulfill the object interface and be passed in directly + err := authz.Authorize(user, authz.ResourceWorkspace.Org("default").Owner(user.UserID), authz.ActionRead) + require.NoError(t, err, "this user can their workspace") + + err = authz.Authorize(user, authz.ResourceWorkspace.Org("default").Owner(user.UserID).AsID("1234"), authz.ActionRead) + require.NoError(t, err, "this user can read workspace '1234'") + }) + + t.Run("CreateNewSiteUser", func(t *testing.T) { + err := authz.Authorize(user, authz.ResourceUser, authz.ActionCreate) + var _ = err + //require.Error(t, err, "this user cannot create new users") + }) +} + +func must[r any](v r, err error) r { + if err != nil { + panic(err) + } + return v +} diff --git a/coderd/authz/permission.go b/coderd/authz/permission.go index 96545fdcbe381..a77290e145947 100644 --- a/coderd/authz/permission.go +++ b/coderd/authz/permission.go @@ -1,6 +1,9 @@ package authz -import "strings" +import ( + "golang.org/x/xerrors" + "strings" +) type permLevel string @@ -51,3 +54,67 @@ func (p Permission) String() string { s.WriteString(string(p.Action)) return s.String() } + +func ParsePermissions(perms string) ([]Permission, error) { + permList := strings.Split(perms, ",") + parsed := make([]Permission, 0, len(permList)) + for _, permStr := range permList { + p, err := ParsePermission(strings.TrimSpace(permStr)) + if err != nil { + return nil, xerrors.Errorf("perm '%s': %w", permStr, err) + } + parsed = append(parsed, p) + } + return parsed, nil +} + +func ParsePermission(perm string) (Permission, error) { + parts := strings.Split(perm, ".") + if len(parts) != 4 { + return Permission{}, xerrors.Errorf("permission expects 4 parts, got %d", len(parts)) + } + + level, resType, resID, act := parts[0], parts[1], parts[2], parts[3] + + if len(level) < 2 { + return Permission{}, xerrors.Errorf("permission level is too short: '%s'", parts[0]) + } + sign := level[0] + levelParts := strings.Split(level[1:], ":") + if len(levelParts) > 2 { + return Permission{}, xerrors.Errorf("unsupported level format") + } + + var permission Permission + + switch sign { + case '+': + permission.Sign = true + case '-': + default: + return Permission{}, xerrors.Errorf("sign must be +/-") + } + + switch permLevel(strings.ToLower(levelParts[0])) { + case LevelWildcard: + permission.Level = LevelWildcard + case LevelSite: + permission.Level = LevelSite + case LevelOrg: + permission.Level = LevelOrg + case LevelUser: + permission.Level = LevelUser + default: + return Permission{}, xerrors.Errorf("'%s' is an unsupported level", levelParts[0]) + } + + if len(levelParts) > 1 { + permission.LevelID = levelParts[1] + } + + // might want to check if these are valid resource types and actions. + permission.ResourceType = ResourceType(resType) + permission.ResourceID = resID + permission.Action = Action(act) + return permission, nil +} diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go index bac2fec538737..1beda588be486 100644 --- a/coderd/authz/permission_test.go +++ b/coderd/authz/permission_test.go @@ -35,9 +35,9 @@ func Test_PermissionString(t *testing.T) { LevelID: "", ResourceType: authz.ResourceDevURL, ResourceID: "1234", - Action: authz.ActionWrite, + Action: authz.ActionCreate, }, - Expected: "-user.devurl.1234.write", + Expected: "-user.devurl.1234.create", }, { Name: "OrgID", @@ -56,7 +56,85 @@ func Test_PermissionString(t *testing.T) { for _, c := range testCases { t.Run(c.Name, func(t *testing.T) { require.Equal(t, c.Expected, c.Permission.String()) + perm, err := authz.ParsePermission(c.Expected) + require.NoError(t, err, "parse perm string") + require.Equal(t, c.Permission, perm, "expected perm") + + perms, err := authz.ParsePermissions(c.Expected) + require.NoError(t, err, "parse perms string") + require.Equal(t, c.Permission, perms[0], "expected perm") + require.Len(t, perms, 1, "expect 1 perm") }) } +} + +func Test_ParsePermissions(t *testing.T) { + t.Parallel() + testCases := []struct { + Name string + Str string + Permissions []authz.Permission + ErrStr string + }{ + { + Name: "NoSign", + Str: "site.*.*.*", + ErrStr: "sign must be +/-", + }, + { + Name: "BadLevel", + Str: "+unknown.*.*.*", + ErrStr: "unsupported level", + }, + { + Name: "NotEnoughParts", + Str: "+*.*.*", + ErrStr: "permission expects 4 parts", + }, + { + Name: "ShortLevel", + Str: "*.*.*.*", + ErrStr: "permission level is too short", + }, + { + Name: "BadLevelID", + Str: "org:1234:extra.*.*.*", + ErrStr: "unsupported level format", + }, + { + Name: "GoodSet", + Str: "+org:1234.workspace.5678.read, -site.*.*.create", + Permissions: []authz.Permission{ + { + Sign: true, + Level: "org", + LevelID: "1234", + ResourceType: authz.ResourceWorkspace, + ResourceID: "5678", + Action: authz.ActionRead, + }, + { + Sign: false, + Level: "site", + LevelID: "", + ResourceType: "*", + ResourceID: "*", + Action: authz.ActionCreate, + }, + }, + }, + } + for _, c := range testCases { + t.Run(c.Name, func(t *testing.T) { + perms, err := authz.ParsePermissions(c.Str) + if c.ErrStr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), c.ErrStr, "exp error") + } else { + require.NoError(t, err, "parse error") + require.Equal(t, c.Permissions, perms, "exp perms") + } + }) + } } diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index e2e070d06d644..77b9e12a332de 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -7,6 +7,7 @@ const ( ResourceWorkspace ResourceType = "workspace" ResourceProject ResourceType = "project" ResourceDevURL ResourceType = "devurl" + ResourceUser ResourceType = "user" ) func (t ResourceType) ID() string { diff --git a/coderd/authz/role.go b/coderd/authz/role.go index 6328268b057d6..5e1987033ad74 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -1,6 +1,5 @@ package authz type Role struct { - Level permLevel Permissions []Permission } diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index dde0bf0275a1a..336c57d66c44e 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -26,9 +26,9 @@ type Subject interface { type SubjectTODO struct { UserID string `json:"user_id"` - Site []Role `json:"site_roles"` - Org []Role `json:"org_roles"` - User []Role `json:"user_roles"` + Site []Role `json:"site_roles"` + Org map[string][]Role `json:"org_roles"` + User []Role `json:"user_roles"` } func (s SubjectTODO) ID() string { @@ -39,8 +39,25 @@ func (s SubjectTODO) SiteRoles() ([]Role, error) { return s.Site, nil } -func (s SubjectTODO) OrgRoles() ([]Role, error) { - return s.Org, nil +func (s SubjectTODO) OrgRoles(_ context.Context, orgID string) ([]Role, error) { + v, ok := s.Org[orgID] + if !ok { + // Members not in an org return the negative perm + return []Role{{ + Permissions: []Permission{ + { + Sign: false, + Level: "*", + LevelID: "", + ResourceType: "*", + ResourceID: "*", + Action: "*", + }, + }, + }}, nil + } + + return v, nil } func (s SubjectTODO) UserRoles() ([]Role, error) { From 5698938fe38706c0898fa751087a8a3e3b825c32 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 4 Apr 2022 18:19:57 -0500 Subject: [PATCH 31/42] use fmt over str builder for easier to read --- coderd/authz/permission.go | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/coderd/authz/permission.go b/coderd/authz/permission.go index a77290e145947..ab22338821af2 100644 --- a/coderd/authz/permission.go +++ b/coderd/authz/permission.go @@ -1,6 +1,7 @@ package authz import ( + "fmt" "golang.org/x/xerrors" "strings" ) @@ -31,28 +32,18 @@ type Permission struct { } // String returns the ... string formatted permission. -// A string builder is used to be the most efficient. func (p Permission) String() string { - var s strings.Builder - // This could be 1 more than the actual capacity. But being 1 byte over for capacity is ok. - s.Grow(1 + 4 + len(p.Level) + len(p.LevelID) + len(p.ResourceType) + len(p.ResourceID) + len(p.Action)) + sign := "-" if p.Sign { - s.WriteRune('+') - } else { - s.WriteRune('-') + sign = "+" } - s.WriteString(string(p.Level)) + levelID := "" if p.LevelID != "" { - s.WriteRune(':') - s.WriteString(p.LevelID) + levelID = ":" + p.LevelID } - s.WriteRune('.') - s.WriteString(string(p.ResourceType)) - s.WriteRune('.') - s.WriteString(p.ResourceID) - s.WriteRune('.') - s.WriteString(string(p.Action)) - return s.String() + + return fmt.Sprintf("%s%s%s.%s.%s.%s", + sign, p.Level, levelID, p.ResourceType, p.ResourceID, p.Action) } func ParsePermissions(perms string) ([]Permission, error) { From 75ed8ef15626b600e074f31a5c4a0a0b2a343862 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 4 Apr 2022 23:34:00 +0000 Subject: [PATCH 32/42] Linting --- coderd/authz/authz.go | 3 +-- coderd/authz/authz_test.go | 8 +++++--- coderd/authz/authztest/iterator.go | 3 ++- coderd/authz/authztest/iterator_test.go | 5 +++-- coderd/authz/authztest/level.go | 2 +- coderd/authz/authztest/level_test.go | 5 ++++- coderd/authz/authztest/permissions_test.go | 3 ++- coderd/authz/authztest/role.go | 12 ++++++------ coderd/authz/authztest/role_test.go | 7 +++++-- coderd/authz/authztest/set.go | 4 ++-- coderd/authz/authztest/set_test.go | 3 ++- coderd/authz/example_test.go | 11 ++++++++--- coderd/authz/object.go | 3 +++ coderd/authz/permission.go | 3 ++- coderd/authz/permission_test.go | 9 ++++++++- coderd/authz/resources.go | 5 ++++- coderd/authz/subject.go | 2 +- 17 files changed, 59 insertions(+), 29 deletions(-) diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index 2684a3f62793e..7668903e13813 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -7,7 +7,6 @@ func Authorize(subj Subject, obj Resource, action Action) error { } // AuthorizePermissions runs the authorize function with the raw permissions in a single list. -func AuthorizePermissions(subjID string, permissions []Permission, object Resource, action Action) error { - +func AuthorizePermissions(_ string, _ []Permission, _ Resource, _ Action) error { return nil } diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index c84934f728a51..23dde3ea34d34 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -79,10 +79,11 @@ func Test_ExhaustiveAuthorize(t *testing.T) { // Result: func(pv string) bool { // return strings.Contains(pv, "+") // }, - //}, + // }, } var failedTests int + //nolint:paralleltest for _, c := range testCases { t.Run(c.Name, func(t *testing.T) { for _, o := range c.Objs { @@ -106,7 +107,7 @@ func Test_ExhaustiveAuthorize(t *testing.T) { }) } // TODO: @emyrk when we implement the correct authorize, we can enable this check. - //require.Equal(t, 0, failedTests, fmt.Sprintf("%d tests failed", failedTests)) + // require.Equal(t, 0, failedTests, fmt.Sprintf("%d tests failed", failedTests)) } func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { @@ -248,7 +249,8 @@ func noise(f noiseBits, lvls ...authztest.LevelGroup) *authztest.Role { } if len(rs) == 1 { - return rs[0].(*authztest.Role) + role, _ := rs[0].(*authztest.Role) + return role } return authztest.NewRole(rs...) } diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/authztest/iterator.go index cef2b1956ace1..dfee580332f06 100644 --- a/coderd/authz/authztest/iterator.go +++ b/coderd/authz/authztest/iterator.go @@ -32,6 +32,7 @@ type unionIterator struct { N int } +//nolint:revive func Union(sets ...Set) *unionIterator { var n int for _, s := range sets { @@ -68,7 +69,7 @@ func (si *unionIterator) Reset() { si.offset = 0 } -func (si *unionIterator) ReturnSize() int { +func (unionIterator) ReturnSize() int { return 1 } diff --git a/coderd/authz/authztest/iterator_test.go b/coderd/authz/authztest/iterator_test.go index 4182c423b6a7d..798d335097b29 100644 --- a/coderd/authz/authztest/iterator_test.go +++ b/coderd/authz/authztest/iterator_test.go @@ -3,11 +3,12 @@ package authztest_test import ( "testing" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/coder/coder/coderd/authz" "github.com/coder/coder/coderd/authz/authztest" crand "github.com/coder/coder/cryptorand" - "github.com/google/uuid" - "github.com/stretchr/testify/require" ) func TestUnion(t *testing.T) { diff --git a/coderd/authz/authztest/level.go b/coderd/authz/authztest/level.go index a78b44be9fc2c..a1d5845d705f3 100644 --- a/coderd/authz/authztest/level.go +++ b/coderd/authz/authztest/level.go @@ -25,7 +25,7 @@ func (lg LevelGroup) All() Set { var i int i += copy(all[i:], pos) i += copy(all[i:], neg) - i += copy(all[i:], net) + copy(all[i:], net) return all } diff --git a/coderd/authz/authztest/level_test.go b/coderd/authz/authztest/level_test.go index 5c3b810a5f0d5..91f6e7ae5ff95 100644 --- a/coderd/authz/authztest/level_test.go +++ b/coderd/authz/authztest/level_test.go @@ -3,9 +3,10 @@ package authztest_test import ( "testing" + "github.com/stretchr/testify/require" + "github.com/coder/coder/coderd/authz" "github.com/coder/coder/coderd/authz/authztest" - "github.com/stretchr/testify/require" ) func Test_GroupedPermissions(t *testing.T) { @@ -81,7 +82,9 @@ func Test_GroupedPermissions(t *testing.T) { } for _, c := range cases { + c := c t.Run(c.Name, func(t *testing.T) { + t.Parallel() require.Equal(t, c.ExpPos+c.ExpNeg+c.ExpAbs, len(c.Lvl.All()), "set size") require.Equal(t, c.ExpPos, len(c.Lvl.Positive()), "correct num pos") require.Equal(t, c.ExpNeg, len(c.Lvl.Negative()), "correct num neg") diff --git a/coderd/authz/authztest/permissions_test.go b/coderd/authz/authztest/permissions_test.go index f169e040fd285..e387ad934e71f 100644 --- a/coderd/authz/authztest/permissions_test.go +++ b/coderd/authz/authztest/permissions_test.go @@ -3,8 +3,9 @@ package authztest_test import ( "testing" - "github.com/coder/coder/coderd/authz/authztest" "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/authz/authztest" ) func Test_AllPermissions(t *testing.T) { diff --git a/coderd/authz/authztest/role.go b/coderd/authz/authztest/role.go index 4a8cf14cb7e1e..f97f50bfe42bf 100644 --- a/coderd/authz/authztest/role.go +++ b/coderd/authz/authztest/role.go @@ -21,7 +21,7 @@ type Role struct { func NewRole(sets ...Iterable) *Role { setInterfaces := make([]Iterator, 0, len(sets)) var retSize int - var size int = 1 + size := 1 for _, s := range sets { v := s.Iterator() setInterfaces = append(setInterfaces, v) @@ -71,11 +71,11 @@ func (r *Role) Next() bool { for i := range r.PermissionSets { if r.PermissionSets[i].Next() { break - } else { - r.PermissionSets[i].Reset() - if i == len(r.PermissionSets)-1 { - return false - } + } + + r.PermissionSets[i].Reset() + if i == len(r.PermissionSets)-1 { + return false } } return true diff --git a/coderd/authz/authztest/role_test.go b/coderd/authz/authztest/role_test.go index 3d884e4b86415..e1531a8a39c72 100644 --- a/coderd/authz/authztest/role_test.go +++ b/coderd/authz/authztest/role_test.go @@ -3,15 +3,18 @@ package authztest_test import ( "testing" + "github.com/stretchr/testify/require" + "github.com/coder/coder/coderd/authz/authztest" crand "github.com/coder/coder/cryptorand" - "github.com/stretchr/testify/require" ) func Test_NewRole(t *testing.T) { + t.Parallel() + for i := 0; i < 50; i++ { sets := make([]authztest.Iterable, 1+(i%4)) - var total int = 1 + total := 1 for j := range sets { size := 1 + must(crand.Intn(3)) if i < 5 { diff --git a/coderd/authz/authztest/set.go b/coderd/authz/authztest/set.go index de11f6e581ead..4b669effa5a82 100644 --- a/coderd/authz/authztest/set.go +++ b/coderd/authz/authztest/set.go @@ -33,8 +33,8 @@ func (s Set) String() string { if v == nil { continue } - str.WriteString(sep) - str.WriteString(v.String()) + _, _ = str.WriteString(sep) + _, _ = str.WriteString(v.String()) sep = ", " } return str.String() diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go index e922f573d5b83..38cbbd32960f9 100644 --- a/coderd/authz/authztest/set_test.go +++ b/coderd/authz/authztest/set_test.go @@ -3,10 +3,11 @@ package authztest_test import ( "testing" + "github.com/stretchr/testify/require" + "github.com/coder/coder/coderd/authz" "github.com/coder/coder/coderd/authz/authztest" crand "github.com/coder/coder/cryptorand" - "github.com/stretchr/testify/require" ) func Test_Set(t *testing.T) { diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index 04427cf30c63e..9afb5f13dec87 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -3,8 +3,9 @@ package authz_test import ( "testing" - "github.com/coder/coder/coderd/authz" "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/authz" ) // Test_Example gives some examples on how to use the authz library. @@ -30,19 +31,22 @@ func Test_Example(t *testing.T) { // TODO: Uncomment all assertions when implementation is done. + //nolint:paralleltest t.Run("ReadAllWorkspaces", func(t *testing.T) { // To read all workspaces on the site err := authz.Authorize(user, authz.ResourceWorkspace, authz.ActionRead) var _ = err - //require.Error(t, err, "this user cannot read all workspaces") + // require.Error(t, err, "this user cannot read all workspaces") }) + //nolint:paralleltest t.Run("ReadOrgWorkspaces", func(t *testing.T) { // To read all workspaces on the org 'default' err := authz.Authorize(user, authz.ResourceWorkspace.Org("default"), authz.ActionRead) require.NoError(t, err, "this user can read all org workspaces in 'default'") }) + //nolint:paralleltest t.Run("ReadMyWorkspace", func(t *testing.T) { // Note 'database.Workspace' could fulfill the object interface and be passed in directly err := authz.Authorize(user, authz.ResourceWorkspace.Org("default").Owner(user.UserID), authz.ActionRead) @@ -52,10 +56,11 @@ func Test_Example(t *testing.T) { require.NoError(t, err, "this user can read workspace '1234'") }) + //nolint:paralleltest t.Run("CreateNewSiteUser", func(t *testing.T) { err := authz.Authorize(user, authz.ResourceUser, authz.ActionCreate) var _ = err - //require.Error(t, err, "this user cannot create new users") + // require.Error(t, err, "this user cannot create new users") }) } diff --git a/coderd/authz/object.go b/coderd/authz/object.go index d3b9486b8f12a..829c8ac6772fd 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -51,17 +51,20 @@ func (z zObject) OrgOwnerID() string { } // Org adds an org OwnerID to the resource +//nolint:revive func (z zObject) Org(orgID string) zObject { z.OwnedByOrg = orgID return z } // Owner adds an OwnerID to the resource +//nolint:revive func (z zObject) Owner(id string) zObject { z.OwnedBy = id return z } +//nolint:revive func (z zObject) AsID(id string) zObject { z.ObjectID = id return z diff --git a/coderd/authz/permission.go b/coderd/authz/permission.go index ab22338821af2..60a16745bff4c 100644 --- a/coderd/authz/permission.go +++ b/coderd/authz/permission.go @@ -2,8 +2,9 @@ package authz import ( "fmt" - "golang.org/x/xerrors" "strings" + + "golang.org/x/xerrors" ) type permLevel string diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go index 1beda588be486..72889a38180cb 100644 --- a/coderd/authz/permission_test.go +++ b/coderd/authz/permission_test.go @@ -3,8 +3,9 @@ package authz_test import ( "testing" - "github.com/coder/coder/coderd/authz" "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/authz" ) func Test_PermissionString(t *testing.T) { @@ -54,7 +55,10 @@ func Test_PermissionString(t *testing.T) { } for _, c := range testCases { + c := c t.Run(c.Name, func(t *testing.T) { + t.Parallel() + require.Equal(t, c.Expected, c.Permission.String()) perm, err := authz.ParsePermission(c.Expected) require.NoError(t, err, "parse perm string") @@ -125,8 +129,11 @@ func Test_ParsePermissions(t *testing.T) { }, }, } + for _, c := range testCases { + c := c t.Run(c.Name, func(t *testing.T) { + t.Parallel() perms, err := authz.ParsePermissions(c.Str) if c.ErrStr != "" { require.Error(t, err) diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index 77b9e12a332de..ba07c39c48ed0 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -10,7 +10,7 @@ const ( ResourceUser ResourceType = "user" ) -func (t ResourceType) ID() string { +func (ResourceType) ID() string { return "" } @@ -19,6 +19,7 @@ func (t ResourceType) ResourceType() ResourceType { } // Org adds an org OwnerID to the resource +//nolint:revive func (r ResourceType) Org(orgID string) zObject { return zObject{ OwnedByOrg: orgID, @@ -27,6 +28,7 @@ func (r ResourceType) Org(orgID string) zObject { } // Owner adds an OwnerID to the resource +//nolint:revive func (r ResourceType) Owner(id string) zObject { return zObject{ OwnedBy: id, @@ -34,6 +36,7 @@ func (r ResourceType) Owner(id string) zObject { } } +//nolint:revive func (r ResourceType) AsID(id string) zObject { return zObject{ ObjectID: id, diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index 336c57d66c44e..86120907f059a 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -64,6 +64,6 @@ func (s SubjectTODO) UserRoles() ([]Role, error) { return s.User, nil } -func (s SubjectTODO) Scopes() ([]Permission, error) { +func (SubjectTODO) Scopes() ([]Permission, error) { return []Permission{}, nil } From b2db661c2fdd44678263b0d9a00c89aabab6c275 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 5 Apr 2022 14:25:44 +0100 Subject: [PATCH 33/42] authz: README.md: update table formatting --- coderd/authz/authztest/README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/coderd/authz/authztest/README.md b/coderd/authz/authztest/README.md index 09edb317a4d02..a49483c41a8c0 100644 --- a/coderd/authz/authztest/README.md +++ b/coderd/authz/authztest/README.md @@ -4,20 +4,20 @@ An authz permission is a combination of `level`, `resource_type`, `resource_id`, A `Set` is a slice of permissions. The search space of all possible sets is too large, so instead this package allows generating more meaningful sets for testing. This is equivalent to pruning in AI problems: a technique to reduce the size of the search space by removing parts that do not have significance. -This is the final pruned search space used in authz. Each set is represented by a ✅, ❌, or ⛶. The leftmost set in a row that is not '⛶' is the impactful set. The impactful set determines the access result. All other sets are non-impactful, and should include the `` permission. The resulting search space for a row is the cross product between all sets in said row. +This is the final pruned search space used in authz. Each set is represented by a Y, N, or _. The leftmost set in a row that is not '_' is the impactful set. The impactful set determines the access result. All other sets are non-impactful, and should include the `` permission. The resulting search space for a row is the cross product between all sets in said row. | Row | * | Site | Org | Org:mem | User | Access | |-----|------|------|------|---------|------|--------| -| W+ | ✅⛶ | ✅❌⛶ | ✅❌⛶ | ✅❌⛶ | ✅❌⛶ | ✅ | -| W- | ❌+✅⛶ | ✅❌⛶ | ✅❌⛶ | ✅❌⛶ | ✅❌⛶ | ❌ | -| S+ | ⛶ | ✅⛶ | ✅❌⛶ | ❌✅⛶ | ❌✅⛶ | ✅ | -| S- | ⛶ | ❌+✅⛶ | ✅❌⛶ | ❌✅⛶ | ❌✅⛶ | ❌ | -| O+ | ⛶ | ⛶ | ✅⛶ | ❌✅⛶ | ❌✅⛶ | ✅ | -| O- | ⛶ | ⛶ | ❌+✅⛶ | ❌✅⛶ | ❌✅⛶ | ❌ | -| M+ | ⛶ | ⛶ | ⛶ | ✅⛶ | ❌✅⛶ | ✅ | -| M- | ⛶ | ⛶ | ⛶ | ❌+✅⛶ | ❌✅⛶ | ❌ | -| U+ | ⛶ | ⛶ | ⛶ | ⛶ | ✅⛶ | ✅ | -| U- | ⛶ | ⛶ | ⛶ | ⛶ | ❌+✅⛶ | ❌ | -| A+ | ⛶ | ⛶ | ⛶ | ⛶ | ✅+⛶ | ✅ | -| A- | ⛶ | ⛶ | ⛶ | ⛶ | ⛶ | ❌ | +| W+ | Y+_ | YN_ | YN_ | YN_ | YN_ | Y | +| W- | N+Y_ | YN_ | YN_ | YN_ | YN_ | N | +| S+ | _ | Y+_ | YN_ | NY_ | NY_ | Y | +| S- | _ | N+Y_ | YN_ | NY_ | NY_ | N | +| O+ | _ | _ | Y+_ | NY_ | NY_ | Y | +| O- | _ | _ | N+Y_ | NY_ | NY_ | N | +| M+ | _ | _ | _ | Y+_ | NY_ | Y | +| M- | _ | _ | _ | N+Y_ | NY_ | N | +| U+ | _ | _ | _ | _ | Y+_ | Y | +| U- | _ | _ | _ | _ | N+Y_ | N | +| A+ | _ | _ | _ | _ | Y+_ | Y | +| A- | _ | _ | _ | _ | _ | N | From 26ef1e6c0263a4cde1ca97e0ee1799cdd65956ac Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 5 Apr 2022 10:43:00 -0500 Subject: [PATCH 34/42] Make action CRUD Make negate bool default to positive permission --- coderd/authz/action.go | 4 ++-- coderd/authz/authztest/iterator_test.go | 4 ++-- coderd/authz/authztest/level_test.go | 4 ++-- coderd/authz/authztest/permissions.go | 10 ++++----- coderd/authz/authztest/set_test.go | 4 ++-- coderd/authz/permission.go | 29 ++++++++++++------------- coderd/authz/permission_test.go | 14 ++++++------ coderd/authz/subject.go | 2 +- 8 files changed, 35 insertions(+), 36 deletions(-) diff --git a/coderd/authz/action.go b/coderd/authz/action.go index f1469dbe71d19..eb7360ef1d6c9 100644 --- a/coderd/authz/action.go +++ b/coderd/authz/action.go @@ -4,8 +4,8 @@ package authz type Action string const ( - ActionRead = "read" ActionCreate = "create" - ActionModify = "modify" + ActionRead = "read" + ActionUpdate = "update" ActionDelete = "delete" ) diff --git a/coderd/authz/authztest/iterator_test.go b/coderd/authz/authztest/iterator_test.go index 798d335097b29..797955d89e420 100644 --- a/coderd/authz/authztest/iterator_test.go +++ b/coderd/authz/authztest/iterator_test.go @@ -55,11 +55,11 @@ func RandomPermission() authz.Permission { actions := []authz.Action{ authz.ActionRead, authz.ActionCreate, - authz.ActionModify, + authz.ActionUpdate, authz.ActionDelete, } return authz.Permission{ - Sign: must(crand.Intn(2))%2 == 0, + Negate: must(crand.Intn(2))%2 == 0, Level: authz.PermissionLevels[must(crand.Intn(len(authz.PermissionLevels)))], LevelID: uuid.New().String(), ResourceType: authz.ResourceWorkspace, diff --git a/coderd/authz/authztest/level_test.go b/coderd/authz/authztest/level_test.go index 91f6e7ae5ff95..ada13e292ebde 100644 --- a/coderd/authz/authztest/level_test.go +++ b/coderd/authz/authztest/level_test.go @@ -19,7 +19,7 @@ func Test_GroupedPermissions(t *testing.T) { for _, a := range []authz.Action{authz.ActionRead, authztest.OtherOption} { if lvl == authz.LevelOrg { set = append(set, &authz.Permission{ - Sign: s, + Negate: s, Level: lvl, LevelID: "mem", ResourceType: authz.ResourceWorkspace, @@ -28,7 +28,7 @@ func Test_GroupedPermissions(t *testing.T) { total++ } set = append(set, &authz.Permission{ - Sign: s, + Negate: s, Level: lvl, ResourceType: authz.ResourceWorkspace, Action: a, diff --git a/coderd/authz/authztest/permissions.go b/coderd/authz/authztest/permissions.go index 6802382a48b34..379d34ae3f945 100644 --- a/coderd/authz/authztest/permissions.go +++ b/coderd/authz/authztest/permissions.go @@ -31,7 +31,7 @@ func AllPermissions() Set { for _, a := range actions { if l == authz.LevelOrg { all = append(all, &authz.Permission{ - Sign: s, + Negate: s, Level: l, LevelID: PermOrgID, ResourceType: t, @@ -40,7 +40,7 @@ func AllPermissions() Set { }) } all = append(all, &authz.Permission{ - Sign: s, + Negate: s, Level: l, LevelID: "", ResourceType: t, @@ -62,8 +62,8 @@ func Impact(p *authz.Permission) PermissionSet { p.Action == OtherOption { return SetNeutral } - if p.Sign { - return SetPositive + if p.Negate { + return SetNegative } - return SetNegative + return SetPositive } diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go index 38cbbd32960f9..a3f4cfe03a137 100644 --- a/coderd/authz/authztest/set_test.go +++ b/coderd/authz/authztest/set_test.go @@ -49,7 +49,7 @@ func Test_Set(t *testing.T) { set := authztest.Set{ &authz.Permission{ - Sign: true, + Negate: false, Level: authz.LevelOrg, LevelID: "1234", ResourceType: authz.ResourceWorkspace, @@ -58,7 +58,7 @@ func Test_Set(t *testing.T) { }, nil, &authz.Permission{ - Sign: false, + Negate: true, Level: authz.LevelSite, LevelID: "", ResourceType: authz.ResourceWorkspace, diff --git a/coderd/authz/permission.go b/coderd/authz/permission.go index 60a16745bff4c..c0c45d955fc6b 100644 --- a/coderd/authz/permission.go +++ b/coderd/authz/permission.go @@ -7,22 +7,21 @@ import ( "golang.org/x/xerrors" ) -type permLevel string +type PermLevel string const ( - LevelWildcard permLevel = "*" - LevelSite permLevel = "site" - LevelOrg permLevel = "org" - LevelUser permLevel = "user" + LevelWildcard PermLevel = "*" + LevelSite PermLevel = "site" + LevelOrg PermLevel = "org" + LevelUser PermLevel = "user" ) -var PermissionLevels = [4]permLevel{LevelWildcard, LevelSite, LevelOrg, LevelUser} +var PermissionLevels = [4]PermLevel{LevelWildcard, LevelSite, LevelOrg, LevelUser} type Permission struct { - // Sign is positive or negative. - // True = Positive, False = negative - Sign bool - Level permLevel + // Negate makes this a negative permission + Negate bool + Level PermLevel // LevelID is used for identifying a particular org. // org:1234 LevelID string @@ -34,9 +33,9 @@ type Permission struct { // String returns the ... string formatted permission. func (p Permission) String() string { - sign := "-" - if p.Sign { - sign = "+" + sign := "+" + if p.Negate { + sign = "-" } levelID := "" if p.LevelID != "" { @@ -81,13 +80,13 @@ func ParsePermission(perm string) (Permission, error) { switch sign { case '+': - permission.Sign = true case '-': + permission.Negate = true default: return Permission{}, xerrors.Errorf("sign must be +/-") } - switch permLevel(strings.ToLower(levelParts[0])) { + switch PermLevel(strings.ToLower(levelParts[0])) { case LevelWildcard: permission.Level = LevelWildcard case LevelSite: diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go index 72889a38180cb..335bcd145cc4c 100644 --- a/coderd/authz/permission_test.go +++ b/coderd/authz/permission_test.go @@ -19,7 +19,7 @@ func Test_PermissionString(t *testing.T) { { Name: "BasicPositive", Permission: authz.Permission{ - Sign: true, + Negate: false, Level: authz.LevelSite, LevelID: "", ResourceType: authz.ResourceWorkspace, @@ -31,7 +31,7 @@ func Test_PermissionString(t *testing.T) { { Name: "BasicNegative", Permission: authz.Permission{ - Sign: false, + Negate: true, Level: authz.LevelUser, LevelID: "", ResourceType: authz.ResourceDevURL, @@ -43,14 +43,14 @@ func Test_PermissionString(t *testing.T) { { Name: "OrgID", Permission: authz.Permission{ - Sign: false, + Negate: true, Level: authz.LevelOrg, LevelID: "default", ResourceType: authz.ResourceProject, ResourceID: "456", - Action: authz.ActionModify, + Action: authz.ActionUpdate, }, - Expected: "-org:default.project.456.modify", + Expected: "-org:default.project.456.update", }, } @@ -111,7 +111,7 @@ func Test_ParsePermissions(t *testing.T) { Str: "+org:1234.workspace.5678.read, -site.*.*.create", Permissions: []authz.Permission{ { - Sign: true, + Negate: false, Level: "org", LevelID: "1234", ResourceType: authz.ResourceWorkspace, @@ -119,7 +119,7 @@ func Test_ParsePermissions(t *testing.T) { Action: authz.ActionRead, }, { - Sign: false, + Negate: true, Level: "site", LevelID: "", ResourceType: "*", diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index 86120907f059a..07138ca40912b 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -46,7 +46,7 @@ func (s SubjectTODO) OrgRoles(_ context.Context, orgID string) ([]Role, error) { return []Role{{ Permissions: []Permission{ { - Sign: false, + Negate: true, Level: "*", LevelID: "", ResourceType: "*", From 19aba30b35ffb18864a56b74bcf08e0fdfd608ee Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 5 Apr 2022 10:44:41 -0500 Subject: [PATCH 35/42] LevelID -> OrganizationID --- coderd/authz/authztest/iterator_test.go | 12 ++--- coderd/authz/authztest/level.go | 2 +- coderd/authz/authztest/level_test.go | 10 ++--- coderd/authz/authztest/permissions.go | 24 +++++----- coderd/authz/authztest/set_test.go | 24 +++++----- coderd/authz/permission.go | 10 ++--- coderd/authz/permission_test.go | 60 ++++++++++++------------- coderd/authz/subject.go | 12 ++--- 8 files changed, 77 insertions(+), 77 deletions(-) diff --git a/coderd/authz/authztest/iterator_test.go b/coderd/authz/authztest/iterator_test.go index 797955d89e420..b0a9e416a142a 100644 --- a/coderd/authz/authztest/iterator_test.go +++ b/coderd/authz/authztest/iterator_test.go @@ -59,12 +59,12 @@ func RandomPermission() authz.Permission { authz.ActionDelete, } return authz.Permission{ - Negate: must(crand.Intn(2))%2 == 0, - Level: authz.PermissionLevels[must(crand.Intn(len(authz.PermissionLevels)))], - LevelID: uuid.New().String(), - ResourceType: authz.ResourceWorkspace, - ResourceID: uuid.New().String(), - Action: actions[must(crand.Intn(len(actions)))], + Negate: must(crand.Intn(2))%2 == 0, + Level: authz.PermissionLevels[must(crand.Intn(len(authz.PermissionLevels)))], + OrganizationID: uuid.New().String(), + ResourceType: authz.ResourceWorkspace, + ResourceID: uuid.New().String(), + Action: actions[must(crand.Intn(len(actions)))], } } diff --git a/coderd/authz/authztest/level.go b/coderd/authz/authztest/level.go index a1d5845d705f3..034b8e6204f03 100644 --- a/coderd/authz/authztest/level.go +++ b/coderd/authz/authztest/level.go @@ -56,7 +56,7 @@ func GroupedPermissions(perms Set) SetGroup { groups[LevelSiteKey][m] = append(groups[LevelSiteKey][m], p) case p.Level == authz.LevelOrg: groups[LevelOrgAllKey][m] = append(groups[LevelOrgAllKey][m], p) - if p.LevelID == "" || p.LevelID == "*" { + if p.OrganizationID == "" || p.OrganizationID == "*" { groups[LevelOrgKey][m] = append(groups[LevelOrgKey][m], p) } else { groups[LevelOrgMemKey][m] = append(groups[LevelOrgMemKey][m], p) diff --git a/coderd/authz/authztest/level_test.go b/coderd/authz/authztest/level_test.go index ada13e292ebde..bb361e86b1f21 100644 --- a/coderd/authz/authztest/level_test.go +++ b/coderd/authz/authztest/level_test.go @@ -19,11 +19,11 @@ func Test_GroupedPermissions(t *testing.T) { for _, a := range []authz.Action{authz.ActionRead, authztest.OtherOption} { if lvl == authz.LevelOrg { set = append(set, &authz.Permission{ - Negate: s, - Level: lvl, - LevelID: "mem", - ResourceType: authz.ResourceWorkspace, - Action: a, + Negate: s, + Level: lvl, + OrganizationID: "mem", + ResourceType: authz.ResourceWorkspace, + Action: a, }) total++ } diff --git a/coderd/authz/authztest/permissions.go b/coderd/authz/authztest/permissions.go index 379d34ae3f945..02f9671d39688 100644 --- a/coderd/authz/authztest/permissions.go +++ b/coderd/authz/authztest/permissions.go @@ -31,21 +31,21 @@ func AllPermissions() Set { for _, a := range actions { if l == authz.LevelOrg { all = append(all, &authz.Permission{ - Negate: s, - Level: l, - LevelID: PermOrgID, - ResourceType: t, - ResourceID: i, - Action: a, + Negate: s, + Level: l, + OrganizationID: PermOrgID, + ResourceType: t, + ResourceID: i, + Action: a, }) } all = append(all, &authz.Permission{ - Negate: s, - Level: l, - LevelID: "", - ResourceType: t, - ResourceID: i, - Action: a, + Negate: s, + Level: l, + OrganizationID: "", + ResourceType: t, + ResourceID: i, + Action: a, }) } } diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go index a3f4cfe03a137..72b6d39fec33d 100644 --- a/coderd/authz/authztest/set_test.go +++ b/coderd/authz/authztest/set_test.go @@ -49,21 +49,21 @@ func Test_Set(t *testing.T) { set := authztest.Set{ &authz.Permission{ - Negate: false, - Level: authz.LevelOrg, - LevelID: "1234", - ResourceType: authz.ResourceWorkspace, - ResourceID: "1234", - Action: authz.ActionRead, + Negate: false, + Level: authz.LevelOrg, + OrganizationID: "1234", + ResourceType: authz.ResourceWorkspace, + ResourceID: "1234", + Action: authz.ActionRead, }, nil, &authz.Permission{ - Negate: true, - Level: authz.LevelSite, - LevelID: "", - ResourceType: authz.ResourceWorkspace, - ResourceID: "*", - Action: authz.ActionRead, + Negate: true, + Level: authz.LevelSite, + OrganizationID: "", + ResourceType: authz.ResourceWorkspace, + ResourceID: "*", + Action: authz.ActionRead, }, } diff --git a/coderd/authz/permission.go b/coderd/authz/permission.go index c0c45d955fc6b..ebcb05c34c9df 100644 --- a/coderd/authz/permission.go +++ b/coderd/authz/permission.go @@ -22,9 +22,9 @@ type Permission struct { // Negate makes this a negative permission Negate bool Level PermLevel - // LevelID is used for identifying a particular org. + // OrganizationID is used for identifying a particular org. // org:1234 - LevelID string + OrganizationID string ResourceType ResourceType ResourceID string @@ -38,8 +38,8 @@ func (p Permission) String() string { sign = "-" } levelID := "" - if p.LevelID != "" { - levelID = ":" + p.LevelID + if p.OrganizationID != "" { + levelID = ":" + p.OrganizationID } return fmt.Sprintf("%s%s%s.%s.%s.%s", @@ -100,7 +100,7 @@ func ParsePermission(perm string) (Permission, error) { } if len(levelParts) > 1 { - permission.LevelID = levelParts[1] + permission.OrganizationID = levelParts[1] } // might want to check if these are valid resource types and actions. diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go index 335bcd145cc4c..940da309ed8b5 100644 --- a/coderd/authz/permission_test.go +++ b/coderd/authz/permission_test.go @@ -19,36 +19,36 @@ func Test_PermissionString(t *testing.T) { { Name: "BasicPositive", Permission: authz.Permission{ - Negate: false, - Level: authz.LevelSite, - LevelID: "", - ResourceType: authz.ResourceWorkspace, - ResourceID: "*", - Action: authz.ActionRead, + Negate: false, + Level: authz.LevelSite, + OrganizationID: "", + ResourceType: authz.ResourceWorkspace, + ResourceID: "*", + Action: authz.ActionRead, }, Expected: "+site.workspace.*.read", }, { Name: "BasicNegative", Permission: authz.Permission{ - Negate: true, - Level: authz.LevelUser, - LevelID: "", - ResourceType: authz.ResourceDevURL, - ResourceID: "1234", - Action: authz.ActionCreate, + Negate: true, + Level: authz.LevelUser, + OrganizationID: "", + ResourceType: authz.ResourceDevURL, + ResourceID: "1234", + Action: authz.ActionCreate, }, Expected: "-user.devurl.1234.create", }, { Name: "OrgID", Permission: authz.Permission{ - Negate: true, - Level: authz.LevelOrg, - LevelID: "default", - ResourceType: authz.ResourceProject, - ResourceID: "456", - Action: authz.ActionUpdate, + Negate: true, + Level: authz.LevelOrg, + OrganizationID: "default", + ResourceType: authz.ResourceProject, + ResourceID: "456", + Action: authz.ActionUpdate, }, Expected: "-org:default.project.456.update", }, @@ -111,20 +111,20 @@ func Test_ParsePermissions(t *testing.T) { Str: "+org:1234.workspace.5678.read, -site.*.*.create", Permissions: []authz.Permission{ { - Negate: false, - Level: "org", - LevelID: "1234", - ResourceType: authz.ResourceWorkspace, - ResourceID: "5678", - Action: authz.ActionRead, + Negate: false, + Level: "org", + OrganizationID: "1234", + ResourceType: authz.ResourceWorkspace, + ResourceID: "5678", + Action: authz.ActionRead, }, { - Negate: true, - Level: "site", - LevelID: "", - ResourceType: "*", - ResourceID: "*", - Action: authz.ActionCreate, + Negate: true, + Level: "site", + OrganizationID: "", + ResourceType: "*", + ResourceID: "*", + Action: authz.ActionCreate, }, }, }, diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index 07138ca40912b..7e55394d5ebd0 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -46,12 +46,12 @@ func (s SubjectTODO) OrgRoles(_ context.Context, orgID string) ([]Role, error) { return []Role{{ Permissions: []Permission{ { - Negate: true, - Level: "*", - LevelID: "", - ResourceType: "*", - ResourceID: "*", - Action: "*", + Negate: true, + Level: "*", + OrganizationID: "", + ResourceType: "*", + ResourceID: "*", + Action: "*", }, }, }}, nil From ceee9cd65a49df4f0706ed072abafadd13a2d40f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 5 Apr 2022 20:17:55 +0000 Subject: [PATCH 36/42] feat: authztest: categorize test failures by test name --- coderd/authz/authz_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 23dde3ea34d34..6ac628b2c3d38 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -1,12 +1,14 @@ package authz_test import ( + "fmt" "math/bits" "strings" "testing" "github.com/coder/coder/coderd/authz" "github.com/coder/coder/coderd/authz/authztest" + "github.com/stretchr/testify/require" ) var nilSet = authztest.Set{nil} @@ -82,7 +84,7 @@ func Test_ExhaustiveAuthorize(t *testing.T) { // }, } - var failedTests int + failedTests := make(map[string]int) //nolint:paralleltest for _, c := range testCases { t.Run(c.Name, func(t *testing.T) { @@ -96,9 +98,9 @@ func Test_ExhaustiveAuthorize(t *testing.T) { o, authztest.PermAction) if c.Result(name) && err != nil { - failedTests++ + failedTests[name]++ } else if !c.Result(name) && err == nil { - failedTests++ + failedTests[name]++ } }) v.Reset() @@ -107,7 +109,9 @@ func Test_ExhaustiveAuthorize(t *testing.T) { }) } // TODO: @emyrk when we implement the correct authorize, we can enable this check. - // require.Equal(t, 0, failedTests, fmt.Sprintf("%d tests failed", failedTests)) + for testName, numFailed := range failedTests { + require.Zero(t, failedTests[testName], fmt.Sprintf("%s: %d tests failed", testName, numFailed)) + } } func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { From ee8bf04a184d4c902fca7752bbd5a75c81dd801d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 5 Apr 2022 20:18:54 +0000 Subject: [PATCH 37/42] fixup! feat: authztest: categorize test failures by test name --- coderd/authz/authz_test.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 6ac628b2c3d38..1f00e0072ff0b 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -1,14 +1,12 @@ package authz_test import ( - "fmt" "math/bits" "strings" "testing" "github.com/coder/coder/coderd/authz" "github.com/coder/coder/coderd/authz/authztest" - "github.com/stretchr/testify/require" ) var nilSet = authztest.Set{nil} @@ -109,9 +107,9 @@ func Test_ExhaustiveAuthorize(t *testing.T) { }) } // TODO: @emyrk when we implement the correct authorize, we can enable this check. - for testName, numFailed := range failedTests { - require.Zero(t, failedTests[testName], fmt.Sprintf("%s: %d tests failed", testName, numFailed)) - } + // for testName, numFailed := range failedTests { + // require.Zero(t, failedTests[testName], fmt.Sprintf("%s: %d tests failed", testName, numFailed)) + // } } func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { From 44c02a13b33f4c6b1973161a3a3c4d3d7b42be69 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 6 Apr 2022 13:28:19 +0000 Subject: [PATCH 38/42] chore: add documentation for authz and authztest --- coderd/authz/README.md | 73 ++++++++++++++++ coderd/authz/authztest/README.md | 138 ++++++++++++++++++++++++++++++- 2 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 coderd/authz/README.md diff --git a/coderd/authz/README.md b/coderd/authz/README.md new file mode 100644 index 0000000000000..cac3b55ac4aff --- /dev/null +++ b/coderd/authz/README.md @@ -0,0 +1,73 @@ +# Authz + +Package `authz` implements AuthoriZation for Coder. + +## Overview + +Authorization defines what **permission** an **subject** has to perform **actions** to **resources**: +- **Permission** is binary: *yes* (allowed) or *no* (denied). +- **Subject** in this case is anything that implements interface `authz.Subject`. +- **Action** here is an enumerated list of actions, but we stick to `Create`, `Read`, `Update`, and `Delete` here. +- **Resource** here is anything that implements `authz.Resource`. + +## Permission Structure + +A **permission** is a rule that grants or denies access for a **subject** to perform an **action** on a **resource**. +A **permission** is always applied at a given **level**: + +- **site** level applies to all resources in a given Coder deployment. +- **org** level applies to all resources that have an organization owner (`org_owner`) +- **user** level applies to all resources that have an owner with the same ID as the subject. + +**Permissions** at a higher **level** always override permissions at a **lower** level. + +The effect of a **permission** can be: +- **positive** (allows) +- **negative** (denies) +- **abstain** (neither allows or denies, but interpreted as deny by default) + +**Negative** permissions **always** override **positive** permissions at the same level. +Both **negative** and **positive** permissions override **abstain** at the same level. + +This can be represented by the following truth table, where Y represents *positive*, N represents *negative*, and _ represents *abstain*: + +| Action | Positive | Negative | Result | +|--------|----------|----------|--------| +| read | Y | _ | Y | +| read | Y | N | N | +| read | _ | _ | _ | +| read | _ | N | Y | + + +## Permission Representation + +**Permissions** are represented in string format as `?...`, where: + +- `sign` can be either `+` or `-`. If it is omitted, sign is assumed to be `+`. +- `level` is either `*`, `site`, `org`, or `user`. +- `resource` is any valid resource type. +- `id` is any valid UUID v4. +- `action` is `create`, `read`, `modify`, or `delete`. + +## Example Permissions + +- `+site.devurl.*.read`: allowed to perform the `read` action against all resources of type `devurl` in a given Coder deployment. +- `-user.workspace.*.create`: user is not allowed to create workspaces. + +## Roles + +A *role* is a set of permissions. When evaluating a role's permission to form an action, all the relevant permissions for the role are combined at each level. Permissions at a higher level override permissions at a lower level. + +The following table shows the per-level role evaluation. +Y indicates that the role provides positive permissions, N indicates the role provides negative permissions, and _ indicates the role does not provide positive or negative permissions. YN_ indicates that the value in the cell does not matter for the access result. + +| Role (example) | Site | Org | User | Result | +|-----------------|------|-----|------|--------| +| site-admin | Y | YN_ | YN_ | Y | +| no-permission | N | YN_ | YN_ | N | +| org-admin | _ | Y | YN_ | Y | +| non-org-member | _ | N | YN_ | N | +| user | _ | _ | Y | Y | +| | _ | _ | N | N | +| unauthenticated | _ | _ | _ | N | + diff --git a/coderd/authz/authztest/README.md b/coderd/authz/authztest/README.md index a49483c41a8c0..1f900ee9ef159 100644 --- a/coderd/authz/authztest/README.md +++ b/coderd/authz/authztest/README.md @@ -1,11 +1,93 @@ # Authztest -An authz permission is a combination of `level`, `resource_type`, `resource_id`, and `action`. For testing purposes, we can assume only 1 action and resource exists. This package can generate all possible permissions from this. +Package `authztest` implements _exhaustive_ unit testing for the `authz` package. -A `Set` is a slice of permissions. The search space of all possible sets is too large, so instead this package allows generating more meaningful sets for testing. This is equivalent to pruning in AI problems: a technique to reduce the size of the search space by removing parts that do not have significance. +## Why this exists + +The `authz.Authorize` function has three* inputs: +- Subject (for example, a user or API key) +- Resource (for example, a workspace or a DevURL) +- Action (for example, read or write). + +**Not including the ruleset, which we're keeping static for the moment.* + +Normally to test a pure function like this, you'd write a table test with all of the permutations by hand, for example: + +```go +func Test_Authorize(t *testing.T) { + .... + testCases := []struct { + name string + subject authz.Subject + resource authz.Resource + action authz.Action + expectedError error + }{ + { + name: "site admin can write config", + subject: &User{ID: "admin"}, + object: &authz.ZObject{ + OrgOwner: "default", + ResourceType: authz.ResourceSiteConfig, + }, + expectedError: nil, + }, + ... + } + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { ... }) + } +} +``` + +This approach is problematic because of the cardinality of the RBAC model. + +Recall that the legacy `pkg/access/authorize`: +- Exposes 8 possible actions, 5 possible site-level roles, 4 possible org-level roles, and 24 possible resource types +- Enforces site-wide versus organization-wide permissions separately + +The new authentication model must maintain backward compatibility with this model, whilst allowing additional features such as: +- User-level ownership (which means user-level permission enforcement) +- Resources shared between users (which means permissions granular down to resource IDs) +- Custom roles + +The resulting permissions model ([documented in Notion](https://www.notion.so/coderhq/Workspaces-V2-Authz-RBAC-24fd193386eb4cf79a282a2a69e8f917)) results in a large **finite** solution space in the order of **hundreds of millions**. + +We want to have a high level of confidence that changes to the implementation **do not have unintended side-effects**. +This means that simply manually writing a set of test cases possibly risks errors slipping through the cracks. + +Instead, we generate (almost) all possible sets of inputs to the library, and ensure that `authz.Authorize` performs as expected. + +The actual investigation of the solution space is [documented in Notion](https://www.notion.so/coderhq/Authz-Exhaustive-Testing-7683ea694c6e4c12ab0124439916b13a), but the crucial take-away of that document is: +- There is a **large** but **finite** number of possible inputs to `authz.Authorize`, +- The solution space can be broken down into 9 groups, and +- Most importantly, *each group has the same expected result.* + +## Testing Methodology + + +We group the search space into a number of groups. Each group corresponds to a set of test cases with the same expected result. Each group consists of a set of **impactful** permissions and a set of **noise** permissions. + +**Impactful** permissions are the top-level permissions that are expected to override anything else, and should be the only inputs that determine the expected result. +**Noise** is simply a set of additional permissions at a lower level that *should not* be impactful. + +For each group, we take the **impactful set** of permissions, and add **noise**, and combine this into a role. + +We then take the *set cross-product* of the **impactful set** and the **noise**, and assert that the expected access level of that role to perform a given action. + +As some of these sets are quite large, we sample some of the noise to reduce the search space. + +TODO: example. + +## Permission Permutations + +Recall that we define a permission as a 4-tuple of `(level, resource_type, resource_id, action)` (for example, `(site, workspace, 123, read)`). + +A `Set` is a slice of permissions. The search space of all possible permissions is too large, so instead this package allows generating more meaningful sets for testing. This is equivalent to pruning in AI problems: a technique to reduce the size of the search space by removing parts that do not have significance. This is the final pruned search space used in authz. Each set is represented by a Y, N, or _. The leftmost set in a row that is not '_' is the impactful set. The impactful set determines the access result. All other sets are non-impactful, and should include the `` permission. The resulting search space for a row is the cross product between all sets in said row. + | Row | * | Site | Org | Org:mem | User | Access | |-----|------|------|------|---------|------|--------| | W+ | Y+_ | YN_ | YN_ | YN_ | YN_ | Y | @@ -21,3 +103,55 @@ This is the final pruned search space used in authz. Each set is represented by | A+ | _ | _ | _ | _ | Y+_ | Y | | A- | _ | _ | _ | _ | _ | N | +Each row in the above table corresponds to a set of test cases. These are described in the next section. + +## Test Cases + +There are 12 possible permutations. + +### Case 1: W+ + +TODO + +### Case 2: W- + +TODO + +### Case 3: S+ + +TODO + +### Case 4: S- + +TODO + +### Case 5: O+ + +TODO + +### Case 6: O- + +TODO + +### Case 7: M+ + +TODO + +### Case 8: M- + +TODO + +### Case 9: U+ + +TODO + +### Case 10: U- + +TODO + +### Case 11: A+ + +TODO +### Case 12: A- + +TODO From dfb9ad1d604ac7d276c6e2e9dac829d9eb459e53 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 6 Apr 2022 14:03:51 +0000 Subject: [PATCH 39/42] fixup! chore: add documentation for authz and authztest --- coderd/authz/README.md | 2 +- coderd/authz/authztest/README.md | 140 +++++++++++++------------------ 2 files changed, 57 insertions(+), 85 deletions(-) diff --git a/coderd/authz/README.md b/coderd/authz/README.md index cac3b55ac4aff..69a8f3d89413e 100644 --- a/coderd/authz/README.md +++ b/coderd/authz/README.md @@ -58,7 +58,7 @@ This can be represented by the following truth table, where Y represents *positi A *role* is a set of permissions. When evaluating a role's permission to form an action, all the relevant permissions for the role are combined at each level. Permissions at a higher level override permissions at a lower level. -The following table shows the per-level role evaluation. +The following table shows the per-level role evaluation logic. Y indicates that the role provides positive permissions, N indicates the role provides negative permissions, and _ indicates the role does not provide positive or negative permissions. YN_ indicates that the value in the cell does not matter for the access result. | Role (example) | Site | Org | User | Result | diff --git a/coderd/authz/authztest/README.md b/coderd/authz/authztest/README.md index 1f900ee9ef159..2ba019056d798 100644 --- a/coderd/authz/authztest/README.md +++ b/coderd/authz/authztest/README.md @@ -1,18 +1,12 @@ # Authztest - Package `authztest` implements _exhaustive_ unit testing for the `authz` package. - ## Why this exists - The `authz.Authorize` function has three* inputs: - Subject (for example, a user or API key) - Resource (for example, a workspace or a DevURL) - Action (for example, read or write). - **Not including the ruleset, which we're keeping static for the moment.* - Normally to test a pure function like this, you'd write a table test with all of the permutations by hand, for example: - ```go func Test_Authorize(t *testing.T) { .... @@ -39,55 +33,36 @@ func Test_Authorize(t *testing.T) { } } ``` - This approach is problematic because of the cardinality of the RBAC model. - Recall that the legacy `pkg/access/authorize`: - Exposes 8 possible actions, 5 possible site-level roles, 4 possible org-level roles, and 24 possible resource types - Enforces site-wide versus organization-wide permissions separately - The new authentication model must maintain backward compatibility with this model, whilst allowing additional features such as: - User-level ownership (which means user-level permission enforcement) - Resources shared between users (which means permissions granular down to resource IDs) - Custom roles - The resulting permissions model ([documented in Notion](https://www.notion.so/coderhq/Workspaces-V2-Authz-RBAC-24fd193386eb4cf79a282a2a69e8f917)) results in a large **finite** solution space in the order of **hundreds of millions**. - We want to have a high level of confidence that changes to the implementation **do not have unintended side-effects**. This means that simply manually writing a set of test cases possibly risks errors slipping through the cracks. - Instead, we generate (almost) all possible sets of inputs to the library, and ensure that `authz.Authorize` performs as expected. - The actual investigation of the solution space is [documented in Notion](https://www.notion.so/coderhq/Authz-Exhaustive-Testing-7683ea694c6e4c12ab0124439916b13a), but the crucial take-away of that document is: - There is a **large** but **finite** number of possible inputs to `authz.Authorize`, - The solution space can be broken down into 9 groups, and - Most importantly, *each group has the same expected result.* - ## Testing Methodology - - We group the search space into a number of groups. Each group corresponds to a set of test cases with the same expected result. Each group consists of a set of **impactful** permissions and a set of **noise** permissions. - **Impactful** permissions are the top-level permissions that are expected to override anything else, and should be the only inputs that determine the expected result. **Noise** is simply a set of additional permissions at a lower level that *should not* be impactful. - -For each group, we take the **impactful set** of permissions, and add **noise**, and combine this into a role. - +For each group, we take the **impactful set** of permissions, and add **noise**, and combine this into a role. We then take the *set cross-product* of the **impactful set** and the **noise**, and assert that the expected access level of that role to perform a given action. - As some of these sets are quite large, we sample some of the noise to reduce the search space. - -TODO: example. - -## Permission Permutations - +**Example:** +`+site:resource:abc123:create` will always override `-user:resource:*:*`, `-user:*:abc123:*`, `-org:resource:*:create`, and so on. All permutations of those sorts of noise permissions should never change the expected result. +## Role Permutations Recall that we define a permission as a 4-tuple of `(level, resource_type, resource_id, action)` (for example, `(site, workspace, 123, read)`). - A `Set` is a slice of permissions. The search space of all possible permissions is too large, so instead this package allows generating more meaningful sets for testing. This is equivalent to pruning in AI problems: a technique to reduce the size of the search space by removing parts that do not have significance. - -This is the final pruned search space used in authz. Each set is represented by a Y, N, or _. The leftmost set in a row that is not '_' is the impactful set. The impactful set determines the access result. All other sets are non-impactful, and should include the `` permission. The resulting search space for a row is the cross product between all sets in said row. - - +This is the final pruned search space used in authz. Each set is represented by a Y, N, or _. The leftmost set in a row that is not '_' is the impactful set. The impactful set determines the access result. All other sets are non-impactful, and should include the `` permission. +The resulting search space for a row is the cross product between all sets in said row. `+` indicates the union of two sets. For example, Y+_ indicates the union of all positive permissions and abstain permissions. | Row | * | Site | Org | Org:mem | User | Access | |-----|------|------|------|---------|------|--------| | W+ | Y+_ | YN_ | YN_ | YN_ | YN_ | Y | @@ -102,56 +77,53 @@ This is the final pruned search space used in authz. Each set is represented by | U- | _ | _ | _ | _ | N+Y_ | N | | A+ | _ | _ | _ | _ | Y+_ | Y | | A- | _ | _ | _ | _ | _ | N | - -Each row in the above table corresponds to a set of test cases. These are described in the next section. - -## Test Cases - -There are 12 possible permutations. - -### Case 1: W+ - -TODO - -### Case 2: W- - -TODO - -### Case 3: S+ - -TODO - -### Case 4: S- - -TODO - -### Case 5: O+ - -TODO - -### Case 6: O- - -TODO - -### Case 7: M+ - -TODO - -### Case 8: M- - -TODO - -### Case 9: U+ - -TODO - -### Case 10: U- - -TODO - -### Case 11: A+ - -TODO -### Case 12: A- - -TODO +Each row in the above table corresponds to a set of role permutations. +There are 12 possible groups of role permutations: +- Case 1 (W+): + - Impactful set: positive wildcard permissions. + - Noise: positive, negative, abstain across site, org, org-member, and user levels. + - Expected result: allow. +- Case 2 (W-): + - Impactful set: negative wildcard permissions. + - Noise: positive, negative, abstain across site, org, org-member, and user levels. + - Expected result: deny. +- Case 3 (S+): + - Impactful set: positive site-level permissions. + - Noise: positive, negative, abstain across org, org-member, and user levels. + - Expected result: allow. +- Case 4 (S-): + - Impactful set: negative site-level permissions. + - Noise: positive, negative, abstain across org, org-member, and user levels. + - Expected result: deny. +- Case 5 (O+): + - Impactful set: positive org-level permissions. + - Noise: positive, negative, abstain across org-member and user levels. + - Expected result: allow. +- Case 6 (O-): + - Impactful set: negative org-level permissions. + - Noise: positive, negative, abstain across org-member and user levels. + - Expected result: deny. +- Case 7 (M+): + - Impactful set: positive org-member permissions. + - Noise: positive, negative, abstain on user level. + - Expected result: allow. +- Case 8 (M-): + - Impactful set: negative org-member permissions. + - Noise: positive, negative, abstain on user level. + - Expected result: deny. +- Case 9 (U+): + - Impactful set: positive user-level permissions. + - Noise: empty set. + - Expected result: allow. +- Case 10 (U-): + - Impactful set: negative user-level permissions. + - Noise: empty set. + - Expected result: deny. +- Case 11 (A+): + - Impactful set: nil permission. + - Noise: positive on user-level. + - Expected result: allow. +- Case 12 (A-): + - Impactful set: nil permission. + - Noise: abstain on user level. + - Expected result: deny. From e482d2c0a9a39289dca13dfc11f5d1c84adc7724 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 6 Apr 2022 15:07:18 +0000 Subject: [PATCH 40/42] chore: more authz/authztest docs --- coderd/authz/README.md | 22 +++++------ coderd/authz/authztest/README.md | 67 ++++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 19 deletions(-) diff --git a/coderd/authz/README.md b/coderd/authz/README.md index 69a8f3d89413e..9f8d5c70b0edd 100644 --- a/coderd/authz/README.md +++ b/coderd/authz/README.md @@ -4,27 +4,27 @@ Package `authz` implements AuthoriZation for Coder. ## Overview -Authorization defines what **permission** an **subject** has to perform **actions** to **resources**: +Authorization defines what **permission** an **subject** has to perform **actions** to **objects**: - **Permission** is binary: *yes* (allowed) or *no* (denied). - **Subject** in this case is anything that implements interface `authz.Subject`. - **Action** here is an enumerated list of actions, but we stick to `Create`, `Read`, `Update`, and `Delete` here. -- **Resource** here is anything that implements `authz.Resource`. +- **Object** here is anything that implements `authz.Object`. ## Permission Structure -A **permission** is a rule that grants or denies access for a **subject** to perform an **action** on a **resource**. +A **permission** is a rule that grants or denies access for a **subject** to perform an **action** on a **object**. A **permission** is always applied at a given **level**: -- **site** level applies to all resources in a given Coder deployment. -- **org** level applies to all resources that have an organization owner (`org_owner`) -- **user** level applies to all resources that have an owner with the same ID as the subject. +- **site** level applies to all objects in a given Coder deployment. +- **org** level applies to all objects that have an organization owner (`org_owner`) +- **user** level applies to all objects that have an owner with the same ID as the subject. **Permissions** at a higher **level** always override permissions at a **lower** level. The effect of a **permission** can be: - **positive** (allows) - **negative** (denies) -- **abstain** (neither allows or denies, but interpreted as deny by default) +- **abstain** (neither allows or denies, not applicable) **Negative** permissions **always** override **positive** permissions at the same level. Both **negative** and **positive** permissions override **abstain** at the same level. @@ -41,24 +41,24 @@ This can be represented by the following truth table, where Y represents *positi ## Permission Representation -**Permissions** are represented in string format as `?...`, where: +**Permissions** are represented in string format as `?...`, where: - `sign` can be either `+` or `-`. If it is omitted, sign is assumed to be `+`. - `level` is either `*`, `site`, `org`, or `user`. -- `resource` is any valid resource type. +- `object` is any valid resource type. - `id` is any valid UUID v4. - `action` is `create`, `read`, `modify`, or `delete`. ## Example Permissions -- `+site.devurl.*.read`: allowed to perform the `read` action against all resources of type `devurl` in a given Coder deployment. +- `+site.*.*.read`: allowed to perform the `read` action against all objects of type `devurl` in a given Coder deployment. - `-user.workspace.*.create`: user is not allowed to create workspaces. ## Roles A *role* is a set of permissions. When evaluating a role's permission to form an action, all the relevant permissions for the role are combined at each level. Permissions at a higher level override permissions at a lower level. -The following table shows the per-level role evaluation logic. +The following table shows the per-level role evaluation. Y indicates that the role provides positive permissions, N indicates the role provides negative permissions, and _ indicates the role does not provide positive or negative permissions. YN_ indicates that the value in the cell does not matter for the access result. | Role (example) | Site | Org | User | Result | diff --git a/coderd/authz/authztest/README.md b/coderd/authz/authztest/README.md index 2ba019056d798..f20b2ddd54481 100644 --- a/coderd/authz/authztest/README.md +++ b/coderd/authz/authztest/README.md @@ -1,19 +1,23 @@ # Authztest Package `authztest` implements _exhaustive_ unit testing for the `authz` package. + ## Why this exists The `authz.Authorize` function has three* inputs: - Subject (for example, a user or API key) -- Resource (for example, a workspace or a DevURL) +- Object (for example, a workspace or a DevURL) - Action (for example, read or write). + **Not including the ruleset, which we're keeping static for the moment.* + Normally to test a pure function like this, you'd write a table test with all of the permutations by hand, for example: + ```go func Test_Authorize(t *testing.T) { .... testCases := []struct { name string subject authz.Subject - resource authz.Resource + resource authz.Object action authz.Action expectedError error }{ @@ -22,7 +26,7 @@ func Test_Authorize(t *testing.T) { subject: &User{ID: "admin"}, object: &authz.ZObject{ OrgOwner: "default", - ResourceType: authz.ResourceSiteConfig, + ObjectType: authz.ObjectSiteConfig, }, expectedError: nil, }, @@ -33,36 +37,63 @@ func Test_Authorize(t *testing.T) { } } ``` + This approach is problematic because of the cardinality of the RBAC model. + Recall that the legacy `pkg/access/authorize`: + - Exposes 8 possible actions, 5 possible site-level roles, 4 possible org-level roles, and 24 possible resource types - Enforces site-wide versus organization-wide permissions separately + The new authentication model must maintain backward compatibility with this model, whilst allowing additional features such as: + - User-level ownership (which means user-level permission enforcement) -- Resources shared between users (which means permissions granular down to resource IDs) +- Objects shared between users (which means permissions granular down to resource IDs) - Custom roles + The resulting permissions model ([documented in Notion](https://www.notion.so/coderhq/Workspaces-V2-Authz-RBAC-24fd193386eb4cf79a282a2a69e8f917)) results in a large **finite** solution space in the order of **hundreds of millions**. -We want to have a high level of confidence that changes to the implementation **do not have unintended side-effects**. -This means that simply manually writing a set of test cases possibly risks errors slipping through the cracks. + +We want to have a high level of confidence that changes to the implementation **do not have unintended side-effects**. This means that simply manually writing a set of test cases possibly risks errors slipping through the cracks. + Instead, we generate (almost) all possible sets of inputs to the library, and ensure that `authz.Authorize` performs as expected. + The actual investigation of the solution space is [documented in Notion](https://www.notion.so/coderhq/Authz-Exhaustive-Testing-7683ea694c6e4c12ab0124439916b13a), but the crucial take-away of that document is: - There is a **large** but **finite** number of possible inputs to `authz.Authorize`, - The solution space can be broken down into 9 groups, and - Most importantly, *each group has the same expected result.* + + ## Testing Methodology + We group the search space into a number of groups. Each group corresponds to a set of test cases with the same expected result. Each group consists of a set of **impactful** permissions and a set of **noise** permissions. + **Impactful** permissions are the top-level permissions that are expected to override anything else, and should be the only inputs that determine the expected result. + **Noise** is simply a set of additional permissions at a lower level that *should not* be impactful. + For each group, we take the **impactful set** of permissions, and add **noise**, and combine this into a role. + We then take the *set cross-product* of the **impactful set** and the **noise**, and assert that the expected access level of that role to perform a given action. + As some of these sets are quite large, we sample some of the noise to reduce the search space. + +We also perform permutation on the **objects** of the test case, explained in [Object Permutations](#object-permutations) + **Example:** -`+site:resource:abc123:create` will always override `-user:resource:*:*`, `-user:*:abc123:*`, `-org:resource:*:create`, and so on. All permutations of those sorts of noise permissions should never change the expected result. + +`+site:*:*:create` will always override `-user:resource:*:*`, `-user:*:abc123:*`, `-org:resource:*:create`, and so on. All permutations of those sorts of noise permissions should never change the expected result. + + ## Role Permutations + Recall that we define a permission as a 4-tuple of `(level, resource_type, resource_id, action)` (for example, `(site, workspace, 123, read)`). + A `Set` is a slice of permissions. The search space of all possible permissions is too large, so instead this package allows generating more meaningful sets for testing. This is equivalent to pruning in AI problems: a technique to reduce the size of the search space by removing parts that do not have significance. -This is the final pruned search space used in authz. Each set is represented by a Y, N, or _. The leftmost set in a row that is not '_' is the impactful set. The impactful set determines the access result. All other sets are non-impactful, and should include the `` permission. + +This is the final pruned search space used in authz. Each set is represented by a Y, N, or \_. The leftmost set in a row that is not '\_' is the impactful set. The impactful set determines the access result. All other sets are non-impactful, and should include the `` permission. + The resulting search space for a row is the cross product between all sets in said row. `+` indicates the union of two sets. For example, Y+_ indicates the union of all positive permissions and abstain permissions. + | Row | * | Site | Org | Org:mem | User | Access | |-----|------|------|------|---------|------|--------| | W+ | Y+_ | YN_ | YN_ | YN_ | YN_ | Y | @@ -77,8 +108,11 @@ The resulting search space for a row is the cross product between all sets in sa | U- | _ | _ | _ | _ | N+Y_ | N | | A+ | _ | _ | _ | _ | Y+_ | Y | | A- | _ | _ | _ | _ | _ | N | + Each row in the above table corresponds to a set of role permutations. + There are 12 possible groups of role permutations: + - Case 1 (W+): - Impactful set: positive wildcard permissions. - Noise: positive, negative, abstain across site, org, org-member, and user levels. @@ -127,3 +161,20 @@ There are 12 possible groups of role permutations: - Impactful set: nil permission. - Noise: abstain on user level. - Expected result: deny. + + +## Object Permutations + +Aside from the test inputs, we also perform permutations on the object. There are 9 possible permuations based on the object, and these 9 test cases all have four distinct possibilities. These are illustrated by the below table: + +| # | Owner | Org-Owner | Result | +|---|---------|-----------|----------------------------------------| +| 1 | `me` | `mem` | Defer | +| 2 | `other` | `mem` | `U+` and `U-` return `false`. | +| 3 | `""` | `mem` | As above. | +| 4 | `me` | `non-mem` | `O+`, `O-`, `U+`, `U-` return `false`. | +| 5 | `other` | `non-mem` | As above. | +| 6 | `other` | `""` | As above. | +| 7 | `""` | `non-mem` | As above. | +| 8 | `""` | `""` | As above. | +| 9 | ` me` | `""` | `O+` and `O-` abstain. Defer to user. | From a4e038f44bd20471a42b7d94f86560e6e538b659 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 6 Apr 2022 10:06:04 -0500 Subject: [PATCH 41/42] Remove underscore from test names unexport fields from zobject --- coderd/authz/authz_test.go | 2 +- coderd/authz/authztest/level_test.go | 2 +- coderd/authz/authztest/permissions_test.go | 2 +- coderd/authz/authztest/role_test.go | 2 +- coderd/authz/authztest/set_test.go | 2 +- coderd/authz/example_test.go | 4 ++-- coderd/authz/object.go | 16 ++++++++-------- coderd/authz/permission_test.go | 4 ++-- coderd/authz/resources.go | 4 ++-- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 1f00e0072ff0b..3b6586bb2697f 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -11,7 +11,7 @@ import ( var nilSet = authztest.Set{nil} -func Test_ExhaustiveAuthorize(t *testing.T) { +func TestExhaustiveAuthorize(t *testing.T) { t.Parallel() all := authztest.GroupedPermissions(authztest.AllPermissions()) diff --git a/coderd/authz/authztest/level_test.go b/coderd/authz/authztest/level_test.go index bb361e86b1f21..3cd4616556e8e 100644 --- a/coderd/authz/authztest/level_test.go +++ b/coderd/authz/authztest/level_test.go @@ -9,7 +9,7 @@ import ( "github.com/coder/coder/coderd/authz/authztest" ) -func Test_GroupedPermissions(t *testing.T) { +func TestGroupedPermissions(t *testing.T) { t.Parallel() set := make(authztest.Set, 0) diff --git a/coderd/authz/authztest/permissions_test.go b/coderd/authz/authztest/permissions_test.go index e387ad934e71f..22fb3fefe7148 100644 --- a/coderd/authz/authztest/permissions_test.go +++ b/coderd/authz/authztest/permissions_test.go @@ -8,7 +8,7 @@ import ( "github.com/coder/coder/coderd/authz/authztest" ) -func Test_AllPermissions(t *testing.T) { +func TestAllPermissions(t *testing.T) { t.Parallel() // If this changes, then we might have to fix some other tests. This constant diff --git a/coderd/authz/authztest/role_test.go b/coderd/authz/authztest/role_test.go index e1531a8a39c72..b630d7aaffba6 100644 --- a/coderd/authz/authztest/role_test.go +++ b/coderd/authz/authztest/role_test.go @@ -9,7 +9,7 @@ import ( crand "github.com/coder/coder/cryptorand" ) -func Test_NewRole(t *testing.T) { +func TestNewRole(t *testing.T) { t.Parallel() for i := 0; i < 50; i++ { diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go index 72b6d39fec33d..88e8dd8276822 100644 --- a/coderd/authz/authztest/set_test.go +++ b/coderd/authz/authztest/set_test.go @@ -10,7 +10,7 @@ import ( crand "github.com/coder/coder/cryptorand" ) -func Test_Set(t *testing.T) { +func TestSet(t *testing.T) { t.Parallel() t.Run("Simple", func(t *testing.T) { diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index 9afb5f13dec87..989673f27f239 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -8,9 +8,9 @@ import ( "github.com/coder/coder/coderd/authz" ) -// Test_Example gives some examples on how to use the authz library. +// TestExample gives some examples on how to use the authz library. // This serves to test syntax more than functionality. -func Test_Example(t *testing.T) { +func TestExample(t *testing.T) { t.Parallel() // user will become an authn object, and can even be a database.User if it diff --git a/coderd/authz/object.go b/coderd/authz/object.go index 829c8ac6772fd..aa4dcb46c1c08 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -25,17 +25,17 @@ var _ OrgResource = (*zObject)(nil) // that represents the set of workspaces you are trying to get access too. // Do not export this type, as it can be created from a resource type constant. type zObject struct { - ObjectID string `json:"object_id"` - OwnedBy string `json:"owner_id"` - OwnedByOrg string `json:"org_owner_id"` + id string + owner string + OwnedByOrg string // ObjectType is "workspace", "project", "devurl", etc - ObjectType ResourceType `json:"object_type"` + ObjectType ResourceType // TODO: SharedUsers? } func (z zObject) ID() string { - return z.ObjectID + return z.id } func (z zObject) ResourceType() ResourceType { @@ -43,7 +43,7 @@ func (z zObject) ResourceType() ResourceType { } func (z zObject) OwnerID() string { - return z.OwnedBy + return z.owner } func (z zObject) OrgOwnerID() string { @@ -60,12 +60,12 @@ func (z zObject) Org(orgID string) zObject { // Owner adds an OwnerID to the resource //nolint:revive func (z zObject) Owner(id string) zObject { - z.OwnedBy = id + z.owner = id return z } //nolint:revive func (z zObject) AsID(id string) zObject { - z.ObjectID = id + z.id = id return z } diff --git a/coderd/authz/permission_test.go b/coderd/authz/permission_test.go index 940da309ed8b5..fd8f161e6573d 100644 --- a/coderd/authz/permission_test.go +++ b/coderd/authz/permission_test.go @@ -8,7 +8,7 @@ import ( "github.com/coder/coder/coderd/authz" ) -func Test_PermissionString(t *testing.T) { +func TestPermissionString(t *testing.T) { t.Parallel() testCases := []struct { @@ -72,7 +72,7 @@ func Test_PermissionString(t *testing.T) { } } -func Test_ParsePermissions(t *testing.T) { +func TestParsePermissions(t *testing.T) { t.Parallel() testCases := []struct { diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index ba07c39c48ed0..7a0a8ce3e5e95 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -31,7 +31,7 @@ func (r ResourceType) Org(orgID string) zObject { //nolint:revive func (r ResourceType) Owner(id string) zObject { return zObject{ - OwnedBy: id, + owner: id, ObjectType: r, } } @@ -39,7 +39,7 @@ func (r ResourceType) Owner(id string) zObject { //nolint:revive func (r ResourceType) AsID(id string) zObject { return zObject{ - ObjectID: id, + id: id, ObjectType: r, } } From 9918c1619b085d3f08045a7c6a90eaa49c4250da Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 6 Apr 2022 10:07:39 -0500 Subject: [PATCH 42/42] zObject does not need exported fields --- coderd/authz/object.go | 16 ++++++++-------- coderd/authz/resources.go | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/coderd/authz/object.go b/coderd/authz/object.go index aa4dcb46c1c08..2c26e9e768bb3 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -25,12 +25,12 @@ var _ OrgResource = (*zObject)(nil) // that represents the set of workspaces you are trying to get access too. // Do not export this type, as it can be created from a resource type constant. type zObject struct { - id string - owner string - OwnedByOrg string + id string + owner string + orgOwner string - // ObjectType is "workspace", "project", "devurl", etc - ObjectType ResourceType + // objectType is "workspace", "project", "devurl", etc + objectType ResourceType // TODO: SharedUsers? } @@ -39,7 +39,7 @@ func (z zObject) ID() string { } func (z zObject) ResourceType() ResourceType { - return z.ObjectType + return z.objectType } func (z zObject) OwnerID() string { @@ -47,13 +47,13 @@ func (z zObject) OwnerID() string { } func (z zObject) OrgOwnerID() string { - return z.OwnedByOrg + return z.orgOwner } // Org adds an org OwnerID to the resource //nolint:revive func (z zObject) Org(orgID string) zObject { - z.OwnedByOrg = orgID + z.orgOwner = orgID return z } diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index 7a0a8ce3e5e95..dd519838415a6 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -22,8 +22,8 @@ func (t ResourceType) ResourceType() ResourceType { //nolint:revive func (r ResourceType) Org(orgID string) zObject { return zObject{ - OwnedByOrg: orgID, - ObjectType: r, + orgOwner: orgID, + objectType: r, } } @@ -32,7 +32,7 @@ func (r ResourceType) Org(orgID string) zObject { func (r ResourceType) Owner(id string) zObject { return zObject{ owner: id, - ObjectType: r, + objectType: r, } } @@ -40,6 +40,6 @@ func (r ResourceType) Owner(id string) zObject { func (r ResourceType) AsID(id string) zObject { return zObject{ id: id, - ObjectType: r, + objectType: r, } }