From ab61328563b943f8b31eabf3b345ffa9ac6a6cc4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 25 Mar 2022 20:21:53 -0500 Subject: [PATCH 01/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] =?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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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/75] 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, } } From 4cf4808856ebc6d0544274b4ad71d2e73db075e8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 7 Apr 2022 20:03:38 +0000 Subject: [PATCH 43/75] checkpoint: crowd programming: define and simplify top-level API --- coderd/authz/authz.go | 8 +- coderd/authz/authz_test.go | 265 +++------------------ coderd/authz/authztest/README.md | 180 -------------- coderd/authz/authztest/doc.go | 2 - coderd/authz/authztest/group.go | 10 - coderd/authz/authztest/iterator.go | 82 ------- coderd/authz/authztest/iterator_test.go | 76 ------ coderd/authz/authztest/level.go | 98 -------- coderd/authz/authztest/level_test.go | 94 -------- coderd/authz/authztest/permissions.go | 69 ------ coderd/authz/authztest/permissions_test.go | 18 -- coderd/authz/authztest/role.go | 88 ------- coderd/authz/authztest/role_test.go | 60 ----- coderd/authz/authztest/set.go | 41 ---- coderd/authz/authztest/set_test.go | 74 ------ coderd/authz/example_test.go | 18 +- coderd/authz/permission.go | 98 -------- coderd/authz/permission_test.go | 147 ------------ coderd/authz/role.go | 28 ++- coderd/authz/subject.go | 28 +-- 20 files changed, 69 insertions(+), 1415 deletions(-) delete mode 100644 coderd/authz/authztest/README.md delete mode 100644 coderd/authz/authztest/doc.go delete mode 100644 coderd/authz/authztest/group.go delete mode 100644 coderd/authz/authztest/iterator.go delete mode 100644 coderd/authz/authztest/iterator_test.go delete mode 100644 coderd/authz/authztest/level.go delete mode 100644 coderd/authz/authztest/level_test.go delete mode 100644 coderd/authz/authztest/permissions.go delete mode 100644 coderd/authz/authztest/permissions_test.go delete mode 100644 coderd/authz/authztest/role.go delete mode 100644 coderd/authz/authztest/role_test.go delete mode 100644 coderd/authz/authztest/set.go delete mode 100644 coderd/authz/authztest/set_test.go delete mode 100644 coderd/authz/permission_test.go diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index 7668903e13813..6b58c08ab34bc 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -1,5 +1,9 @@ package authz +import "errors" + +var ErrUnauthorized = errors.New("unauthorized") + // TODO: Implement Authorize func Authorize(subj Subject, obj Resource, action Action) error { // TODO: Expand subject roles into their permissions as appropriate. Apply scopes. @@ -8,5 +12,7 @@ func Authorize(subj Subject, obj Resource, action Action) error { // AuthorizePermissions runs the authorize function with the raw permissions in a single list. func AuthorizePermissions(_ string, _ []Permission, _ Resource, _ Action) error { - return nil + // return nil + // for now, nothing is allowed + return ErrUnauthorized } diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 3b6586bb2697f..e0f2973ecae69 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -1,258 +1,57 @@ package authz_test import ( - "math/bits" - "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/coder/coder/coderd/authz" - "github.com/coder/coder/coderd/authz/authztest" ) -var nilSet = authztest.Set{nil} - -func TestExhaustiveAuthorize(t *testing.T) { +func TestAuthorize(t *testing.T) { t.Parallel() - all := authztest.GroupedPermissions(authztest.AllPermissions()) - roleVariants := permissionVariants(all) - res := authz.ResourceType(authztest.PermObjectType).AsID(authztest.PermObjectID) - testCases := []struct { - Name string - Objs []authz.Resource - // Action is constant - // Subject comes from roleVariants - Result func(pv string) bool + name string + subject authz.Subject + resource authz.Resource + actions []authz.Action + error string }{ { - Name: "User:Org", - Objs: []authz.Resource{ - res.Owner(authztest.PermMe).Org(authztest.PermOrgID), - }, - Result: func(pv string) bool { - return strings.Contains(pv, "+") + name: "unauthenticated user cannot perform an action", + subject: authz.SubjectTODO{ + UserID: "", + Site: []authz.Role{authz.RoleNoPerm}, }, + resource: authz.ResourceWorkspace, + actions: []authz.Action{authz.ActionRead, authz.ActionCreate, authz.ActionDelete, authz.ActionUpdate}, + error: "unauthorized", }, { - // All U+/- tests should fail - Name: "NotUser:Org", - 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 - } - return strings.Contains(pv, "+") + name: "admin can do anything", + subject: authz.SubjectTODO{ + UserID: "admin", + Site: []authz.Role{authz.RoleAllowAll}, }, + resource: authz.ResourceWorkspace, + actions: []authz.Action{authz.ActionRead, authz.ActionCreate, authz.ActionDelete, authz.ActionUpdate}, + error: "", }, - { - // All O+/- and U+/- tests should fail - Name: "NotUser:NotOrg", - 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 - } - 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, "+") - // }, - // }, } - failedTests := make(map[string]int) - //nolint:paralleltest - for _, c := range testCases { - t.Run(c.Name, func(t *testing.T) { - for _, o := range c.Objs { - 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( - authztest.PermMe, - set.Permissions(), - o, - authztest.PermAction) - if c.Result(name) && err != nil { - failedTests[name]++ - } else if !c.Result(name) && err == nil { - failedTests[name]++ - } - }) - v.Reset() + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + for _, action := range testCase.actions { + err := authz.Authorize(testCase.subject, testCase.resource, action) + if testCase.error == "" { + require.NoError(t, err, "expected no error for testcase testcase %q action %s", testCase.name, action) + continue } + require.EqualError(t, err, testCase.error, "unexpected error") } }) } - // 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)) - // } -} - -func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role { - // an is any noise above the impactful set - an := noiseAbstain - // ln is any noise below the impactful set - ln := noisePositive | noiseNegative | noiseAbstain - - // 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()), - ), - // Org:* -- Added org:mem noise - "O+": authztest.NewRole( - noise(an, all.Wildcard(), all.Site(), all.OrgMem()), - pos(all.Org()), - noise(ln, all.User()), - ), - "O-": authztest.NewRole( - noise(an, all.Wildcard(), all.Site(), all.OrgMem()), - neg(all.Org()), - noise(ln, all.User()), - ), - // Org:Mem -- Added org:* noise - "M+": authztest.NewRole( - noise(an, all.Wildcard(), all.Site(), all.Org()), - pos(all.OrgMem()), - noise(ln, all.User()), - ), - "M-": authztest.NewRole( - noise(an, all.Wildcard(), all.Site(), all.Org()), - 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()), - ), - // 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(), - ), - ), - } -} - -// 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 ( - _ noiseBits = 1 << iota - noisePositive - noiseNegative - noiseAbstain -) - -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(noisePositive, f) { - sets = append(sets, authztest.Union(lvl.Positive()[:1], nilSet)) - } - if flagMatch(noiseNegative, f) { - sets = append(sets, authztest.Union(lvl.Negative()[:1], nilSet)) - } - if flagMatch(noiseAbstain, f) { - sets = append(sets, authztest.Union(lvl.Abstain()[:1], nilSet)) - } - - rs = append(rs, authztest.NewRole( - sets..., - )) - } - - if len(rs) == 1 { - role, _ := rs[0].(*authztest.Role) - return role - } - return authztest.NewRole(rs...) } diff --git a/coderd/authz/authztest/README.md b/coderd/authz/authztest/README.md deleted file mode 100644 index f20b2ddd54481..0000000000000 --- a/coderd/authz/authztest/README.md +++ /dev/null @@ -1,180 +0,0 @@ -# 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) -- 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.Object - action authz.Action - expectedError error - }{ - { - name: "site admin can write config", - subject: &User{ID: "admin"}, - object: &authz.ZObject{ - OrgOwner: "default", - ObjectType: authz.ObjectSiteConfig, - }, - 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) -- 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. - -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:*:*: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. `+` 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 | -| 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 | - -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. - - -## 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. | diff --git a/coderd/authz/authztest/doc.go b/coderd/authz/authztest/doc.go deleted file mode 100644 index 3acc3fcdc1a8c..0000000000000 --- a/coderd/authz/authztest/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package authztest is a helper package for generating permissions to test the authz library. -package authztest diff --git a/coderd/authz/authztest/group.go b/coderd/authz/authztest/group.go deleted file mode 100644 index 585eca6bc0da8..0000000000000 --- a/coderd/authz/authztest/group.go +++ /dev/null @@ -1,10 +0,0 @@ -package authztest - -// PermissionSet defines a set of permissions with the same impact. -type PermissionSet string - -const ( - SetPositive PermissionSet = "j" - SetNegative PermissionSet = "j!" - SetNeutral PermissionSet = "a" -) diff --git a/coderd/authz/authztest/iterator.go b/coderd/authz/authztest/iterator.go deleted file mode 100644 index dfee580332f06..0000000000000 --- a/coderd/authz/authztest/iterator.go +++ /dev/null @@ -1,82 +0,0 @@ -package authztest - -import ( - "github.com/coder/coder/coderd/authz" -) - -type Iterable interface { - Iterator() Iterator -} - -type Iterator interface { - Iterable - - Next() bool - Permissions() Set - Reset() - ReturnSize() int - Size() int -} - -// 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 - // 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 -} - -//nolint:revive -func Union(sets ...Set) *unionIterator { - var n int - for _, s := range sets { - n += len(s) - } - return &unionIterator{ - sets: sets, - buffer: make(Set, 1), - N: n, - } -} - -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 *unionIterator) Permissions() Set { - si.buffer[0] = si.Permission() - return si.buffer -} - -func (si unionIterator) Permission() *authz.Permission { - return si.sets[si.setIdx][si.offset] -} - -func (si *unionIterator) Reset() { - si.setIdx = 0 - si.offset = 0 -} - -func (unionIterator) ReturnSize() int { - return 1 -} - -func (si *unionIterator) Size() int { - return si.N -} - -func (si *unionIterator) Iterator() Iterator { - return si -} diff --git a/coderd/authz/authztest/iterator_test.go b/coderd/authz/authztest/iterator_test.go deleted file mode 100644 index b0a9e416a142a..0000000000000 --- a/coderd/authz/authztest/iterator_test.go +++ /dev/null @@ -1,76 +0,0 @@ -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" -) - -func TestUnion(t *testing.T) { - t.Parallel() - - for i := 0; i < 100; i++ { - allPerms := make(authztest.Set, 0) - // 2 - 4 sets - sets := make([]authztest.Set, 1+must(crand.Intn(2))) - for j := range sets { - sets[j] = RandomSet(1 + must(crand.Intn(4))) - allPerms = append(allPerms, sets[j]...) - } - - 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, 1, len(ui.Permissions()), "permissions size") - require.Equal(t, allPerms[c], ui.Permissions()[0], "permission order") - if !ui.Next() { - break - } - } - - 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 { - actions := []authz.Action{ - authz.ActionRead, - authz.ActionCreate, - authz.ActionUpdate, - authz.ActionDelete, - } - return authz.Permission{ - 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)))], - } -} - -func must[r any](v r, err error) r { - if err != nil { - panic(err) - } - return v -} diff --git a/coderd/authz/authztest/level.go b/coderd/authz/authztest/level.go deleted file mode 100644 index 034b8e6204f03..0000000000000 --- a/coderd/authz/authztest/level.go +++ /dev/null @@ -1,98 +0,0 @@ -package authztest - -import "github.com/coder/coder/coderd/authz" - -type level string - -const ( - 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 -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) - 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{LevelWildKey, LevelSiteKey, LevelOrgKey, LevelOrgMemKey, LevelOrgAllKey, LevelUserKey} - - for _, l := range allLevelKeys { - groups[l] = make(LevelGroup) - } - - for _, p := range perms { - m := Impact(p) - switch { - case p.Level == authz.LevelSite: - groups[LevelSiteKey][m] = append(groups[LevelSiteKey][m], p) - case p.Level == authz.LevelOrg: - groups[LevelOrgAllKey][m] = append(groups[LevelOrgAllKey][m], p) - if p.OrganizationID == "" || p.OrganizationID == "*" { - groups[LevelOrgKey][m] = append(groups[LevelOrgKey][m], p) - } else { - groups[LevelOrgMemKey][m] = append(groups[LevelOrgMemKey][m], p) - } - case p.Level == authz.LevelUser: - groups[LevelUserKey][m] = append(groups[LevelUserKey][m], p) - case p.Level == authz.LevelWildcard: - groups[LevelWildKey][m] = append(groups[LevelWildKey][m], p) - } - } - - return groups -} - -type SetGroup map[level]LevelGroup - -func (s SetGroup) Wildcard() LevelGroup { - return s[LevelWildKey] -} - -func (s SetGroup) Site() LevelGroup { - return s[LevelSiteKey] -} - -func (s SetGroup) Org() LevelGroup { - return s[LevelOrgKey] -} - -func (s SetGroup) AllOrgs() LevelGroup { - return s[LevelOrgAllKey] -} - -func (s SetGroup) OrgMem() LevelGroup { - return s[LevelOrgMemKey] -} - -func (s SetGroup) User() LevelGroup { - return s[LevelUserKey] -} diff --git a/coderd/authz/authztest/level_test.go b/coderd/authz/authztest/level_test.go deleted file mode 100644 index 3cd4616556e8e..0000000000000 --- a/coderd/authz/authztest/level_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package authztest_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/authz" - "github.com/coder/coder/coderd/authz/authztest" -) - -func TestGroupedPermissions(t *testing.T) { - t.Parallel() - - 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.ActionRead, authztest.OtherOption} { - if lvl == authz.LevelOrg { - set = append(set, &authz.Permission{ - Negate: s, - Level: lvl, - OrganizationID: "mem", - ResourceType: authz.ResourceWorkspace, - Action: a, - }) - total++ - } - set = append(set, &authz.Permission{ - Negate: 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 { - 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") - require.Equal(t, c.ExpAbs, len(c.Lvl.Abstain()), "correct num abs") - }) - } -} diff --git a/coderd/authz/authztest/permissions.go b/coderd/authz/authztest/permissions.go deleted file mode 100644 index 02f9671d39688..0000000000000 --- a/coderd/authz/authztest/permissions.go +++ /dev/null @@ -1,69 +0,0 @@ -package authztest - -import ( - "github.com/coder/coder/coderd/authz" -) - -const ( - OtherOption = "other" - PermObjectType = "resource" - PermAction = "read" - PermOrgID = "mem" - PermObjectID = "rid" - PermMe = "me" -) - -var ( - levelIDs = []string{"", PermOrgID} - resourceTypes = []authz.ResourceType{PermObjectType, "*", OtherOption} - resourceIDs = []string{PermObjectID, "*", OtherOption} - actions = []authz.Action{PermAction, "*", OtherOption} -) - -// AllPermissions returns all the possible permissions ever. -func AllPermissions() Set { - permissionTypes := []bool{true, false} - all := make(Set, 0, len(permissionTypes)*len(authz.PermissionLevels)*len(levelIDs)*len(resourceTypes)*len(resourceIDs)*len(actions)) - for _, s := range permissionTypes { - for _, l := range authz.PermissionLevels { - for _, t := range resourceTypes { - for _, i := range resourceIDs { - for _, a := range actions { - if l == authz.LevelOrg { - all = append(all, &authz.Permission{ - Negate: s, - Level: l, - OrganizationID: PermOrgID, - ResourceType: t, - ResourceID: i, - Action: a, - }) - } - all = append(all, &authz.Permission{ - Negate: s, - Level: l, - OrganizationID: "", - ResourceType: t, - ResourceID: i, - Action: a, - }) - } - } - } - } - } - return all -} - -// 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 { - return SetNeutral - } - if p.Negate { - return SetNegative - } - return SetPositive -} diff --git a/coderd/authz/authztest/permissions_test.go b/coderd/authz/authztest/permissions_test.go deleted file mode 100644 index 22fb3fefe7148..0000000000000 --- a/coderd/authz/authztest/permissions_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package authztest_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/authz/authztest" -) - -func TestAllPermissions(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 - require.Equal(t, len(authztest.AllPermissions()), totalUniquePermissions, "expected set size") -} diff --git a/coderd/authz/authztest/role.go b/coderd/authz/authztest/role.go deleted file mode 100644 index f97f50bfe42bf..0000000000000 --- a/coderd/authz/authztest/role.go +++ /dev/null @@ -1,88 +0,0 @@ -package authztest - -import ( - "github.com/coder/coder/coderd/authz" -) - -// 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 weird, but the first scan should not move anything. - first bool - - buffer []*authz.Permission -} - -func NewRole(sets ...Iterable) *Role { - setInterfaces := make([]Iterator, 0, len(sets)) - var retSize int - size := 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([]*authz.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 - for _, ps := range r.PermissionSets { - i += copy(r.buffer[i:], ps.Permissions()) - } - return r.buffer -} - -func (r *Role) Each(ea func(set Set)) { - ea(r.Permissions()) - for r.Next() { - ea(r.Permissions()) - } -} - -// 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() { - break - } - - 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/authztest/role_test.go b/coderd/authz/authztest/role_test.go deleted file mode 100644 index b630d7aaffba6..0000000000000 --- a/coderd/authz/authztest/role_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package authztest_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/authz/authztest" - crand "github.com/coder/coder/cryptorand" -) - -func TestNewRole(t *testing.T) { - t.Parallel() - - for i := 0; i < 50; i++ { - sets := make([]authztest.Iterable, 1+(i%4)) - total := 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) - } - - 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.go b/coderd/authz/authztest/set.go deleted file mode 100644 index 4b669effa5a82..0000000000000 --- a/coderd/authz/authztest/set.go +++ /dev/null @@ -1,41 +0,0 @@ -package authztest - -import ( - "strings" - - "github.com/coder/coder/coderd/authz" -) - -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 { - if s[i] != nil { - perms = append(perms, *s[i]) - } - } - return perms -} - -func (s Set) Iterator() Iterator { - return Union(s) -} - -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 = ", " - } - return str.String() -} diff --git a/coderd/authz/authztest/set_test.go b/coderd/authz/authztest/set_test.go deleted file mode 100644 index 88e8dd8276822..0000000000000 --- a/coderd/authz/authztest/set_test.go +++ /dev/null @@ -1,74 +0,0 @@ -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" -) - -func TestSet(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") - 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) { - t.Parallel() - - 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) { - t.Parallel() - - set := authztest.Set{ - &authz.Permission{ - Negate: false, - Level: authz.LevelOrg, - OrganizationID: "1234", - ResourceType: authz.ResourceWorkspace, - ResourceID: "1234", - Action: authz.ActionRead, - }, - nil, - &authz.Permission{ - Negate: true, - Level: authz.LevelSite, - OrganizationID: "", - ResourceType: authz.ResourceWorkspace, - ResourceID: "*", - Action: authz.ActionRead, - }, - } - - require.Equal(t, - "+org:1234.workspace.1234.read, -site.workspace.*.read", - set.String(), "exp string") - }) -} diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index 989673f27f239..5e25fde9e9614 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -17,16 +17,7 @@ func TestExample(t *testing.T) { // 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.*.*.*"))}, - }, + Site: []authz.Role{authz.RoleReadOnly}, } // TODO: Uncomment all assertions when implementation is done. @@ -63,10 +54,3 @@ func TestExample(t *testing.T) { // 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 ebcb05c34c9df..0147f2f583542 100644 --- a/coderd/authz/permission.go +++ b/coderd/authz/permission.go @@ -1,27 +1,8 @@ package authz -import ( - "fmt" - "strings" - - "golang.org/x/xerrors" -) - -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 { // Negate makes this a negative permission Negate bool - Level PermLevel // OrganizationID is used for identifying a particular org. // org:1234 OrganizationID string @@ -30,82 +11,3 @@ type Permission struct { ResourceID string Action Action } - -// String returns the ... string formatted permission. -func (p Permission) String() string { - sign := "+" - if p.Negate { - sign = "-" - } - levelID := "" - if p.OrganizationID != "" { - levelID = ":" + p.OrganizationID - } - - 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) { - 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 '+': - case '-': - permission.Negate = true - 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.OrganizationID = 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 deleted file mode 100644 index fd8f161e6573d..0000000000000 --- a/coderd/authz/permission_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package authz_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/authz" -) - -func TestPermissionString(t *testing.T) { - t.Parallel() - - testCases := []struct { - Name string - Permission authz.Permission - Expected string - }{ - { - Name: "BasicPositive", - Permission: authz.Permission{ - 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, - OrganizationID: "", - ResourceType: authz.ResourceDevURL, - ResourceID: "1234", - Action: authz.ActionCreate, - }, - Expected: "-user.devurl.1234.create", - }, - { - Name: "OrgID", - Permission: authz.Permission{ - Negate: true, - Level: authz.LevelOrg, - OrganizationID: "default", - ResourceType: authz.ResourceProject, - ResourceID: "456", - Action: authz.ActionUpdate, - }, - Expected: "-org:default.project.456.update", - }, - } - - 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") - 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 TestParsePermissions(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{ - { - Negate: false, - Level: "org", - OrganizationID: "1234", - ResourceType: authz.ResourceWorkspace, - ResourceID: "5678", - Action: authz.ActionRead, - }, - { - Negate: true, - Level: "site", - OrganizationID: "", - ResourceType: "*", - ResourceID: "*", - Action: authz.ActionCreate, - }, - }, - }, - } - - 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) - 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/role.go b/coderd/authz/role.go index 5e1987033ad74..cb8b27812d5cb 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -1,5 +1,27 @@ package authz -type Role struct { - Permissions []Permission -} +type Role []Permission + +var ( + RoleAllowAll = Role{ + { + Negate: false, + OrganizationID: "*", + ResourceType: "*", + ResourceID: "*", + Action: "*", + }, + } + + RoleReadOnly = Role{ + { + Negate: false, + OrganizationID: "*", + ResourceType: "*", + ResourceID: "*", + Action: ActionRead, + }, + } + + RoleNoPerm = Role{} +) diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index 7e55394d5ebd0..f1e48762e598d 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -15,9 +15,6 @@ type Subject interface { // and grabbing all the roles for all orgs is excessive. OrgRoles(ctx context.Context, orgID string) ([]Role, error) UserRoles() ([]Role, error) - - // Scopes can limit the roles above. - Scopes() ([]Permission, error) } // SubjectTODO is a placeholder until we get an actual actor struct in place. @@ -26,9 +23,9 @@ type Subject interface { type SubjectTODO struct { UserID string `json:"user_id"` - Site []Role `json:"site_roles"` - Org map[string][]Role `json:"org_roles"` - User []Role `json:"user_roles"` + Site []Role + Org map[string][]Role + User []Role } func (s SubjectTODO) ID() string { @@ -40,24 +37,7 @@ func (s SubjectTODO) SiteRoles() ([]Role, error) { } 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{ - { - Negate: true, - Level: "*", - OrganizationID: "", - ResourceType: "*", - ResourceID: "*", - Action: "*", - }, - }, - }}, nil - } - - return v, nil + return s.Org[orgID], nil } func (s SubjectTODO) UserRoles() ([]Role, error) { From 359a04de5ea5ef2fc28dd4b567b0736472293d76 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 7 Apr 2022 15:55:53 -0500 Subject: [PATCH 44/75] Add tabled tests for authz --- coderd/authz/action.go | 4 + coderd/authz/authz_test.go | 210 +++++++++++++++++++++++++++++++++++++ coderd/authz/role.go | 38 +++++-- coderd/authz/subject.go | 7 +- 4 files changed, 246 insertions(+), 13 deletions(-) diff --git a/coderd/authz/action.go b/coderd/authz/action.go index eb7360ef1d6c9..518ab2bc2ce19 100644 --- a/coderd/authz/action.go +++ b/coderd/authz/action.go @@ -1,5 +1,9 @@ package authz +func AllActions() []Action { + return []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete} +} + // Action represents the allowed actions to be done on an object. type Action string diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index e0f2973ecae69..f18369d19c693 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -55,3 +55,213 @@ func TestAuthorize(t *testing.T) { }) } } + +// TestAuthorizeBasic test the very basic roles that are commonly used. +func TestAuthorizeBasic(t *testing.T) { + t.Parallel() + defOrg := "default" + defWorkspaceID := "1234" + + user := authz.SubjectTODO{ + UserID: "me", + Site: []authz.Role{}, + Org: map[string][]authz.Role{ + defOrg: {}, + }, + User: []authz.Role{authz.RoleAllowAll}, + } + + testAuthorize(t, "Member", user, []authTestCase{ + // Read my own resources + {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: true}, + // My workspace in my org + {resource: authz.ResourceProject.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceProject.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: authz.AllActions(), allow: true}, + + // Read resources in default org + {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceProject.Org(defOrg), actions: authz.AllActions(), allow: true}, + + // Objs in other orgs + {resource: authz.ResourceWorkspace.Org("other"), actions: authz.AllActions(), allow: false}, + // Obj in other org owned by me + {resource: authz.ResourceProject.Org("other").Owner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceProject.Org("other").Owner(user.ID()).AsID(defWorkspaceID), actions: authz.AllActions(), allow: false}, + + // Site wide + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + }) + + user = authz.SubjectTODO{ + UserID: "me", + Site: []authz.Role{authz.RoleBlockAll}, + Org: map[string][]authz.Role{ + defOrg: {}, + }, + User: []authz.Role{authz.RoleAllowAll}, + } + + testAuthorize(t, "DeletedMember", user, []authTestCase{ + // Read my own resources + {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: false}, + // My workspace in my org + {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: false}, + + // Read resources in default org + {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: false}, + + // Objs in other orgs + {resource: authz.ResourceWorkspace.Org("other"), actions: authz.AllActions(), allow: false}, + // Obj in other org owned by me + {resource: authz.ResourceProject.Org("other").Owner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceProject.Org("other").Owner(user.ID()).AsID("1234"), actions: authz.AllActions(), allow: false}, + + // Site wide + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + }) + + user = authz.SubjectTODO{ + UserID: "me", + Site: []authz.Role{}, + Org: map[string][]authz.Role{ + defOrg: {authz.RoleAllowAll}, + }, + User: []authz.Role{authz.RoleAllowAll}, + } + + testAuthorize(t, "OrgAdmin", user, []authTestCase{ + // Read my own resources + {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: true}, + // My workspace in my org + {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: true}, + // Another workspace in my org + {resource: authz.ResourceWorkspace.Org(defOrg).Owner("other"), actions: authz.AllActions(), allow: true}, + + // Read resources in default org + {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceProject.Org(defOrg), actions: authz.AllActions(), allow: true}, + + // Objs in other orgs + {resource: authz.ResourceWorkspace.Org("other"), actions: authz.AllActions(), allow: false}, + // Obj in other org owned by me + {resource: authz.ResourceProject.Org("other").Owner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceProject.Org("other").Owner(user.ID()).AsID("1234"), actions: authz.AllActions(), allow: false}, + + // Site wide + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + }) + + user = authz.SubjectTODO{ + UserID: "me", + Site: []authz.Role{authz.RoleAllowAll}, + Org: map[string][]authz.Role{}, + User: []authz.Role{authz.RoleAllowAll}, + } + + testAuthorize(t, "SiteAdmin", user, []authTestCase{ + // Read my own resources + {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: true}, + // My workspace in my org + {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: true}, + // Another workspace in my org + {resource: authz.ResourceWorkspace.Org(defOrg).Owner("other"), actions: authz.AllActions(), allow: true}, + + // Read resources in default org + {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceProject.Org(defOrg), actions: authz.AllActions(), allow: true}, + + // Objs in other orgs + {resource: authz.ResourceWorkspace.Org("other"), actions: authz.AllActions(), allow: true}, + // Obj in other org owned by me + {resource: authz.ResourceProject.Org("other").Owner(user.ID()), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceProject.Org("other").Owner(user.ID()).AsID("1234"), actions: authz.AllActions(), allow: true}, + + // Site wide + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: true}, + }) + + // In practice this is a token scope on a regular subject + user = authz.SubjectTODO{ + UserID: "me", + Site: []authz.Role{}, + Org: map[string][]authz.Role{ + defOrg: {}, + }, + User: []authz.Role{authz.WorkspaceAgentRole(defWorkspaceID)}, + } + + testAuthorize(t, "WorkspaceAgentToken", user, []authTestCase{ + // Read workspace by ID + {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: []authz.Action{authz.ActionRead}, allow: true}, + // C_UD + {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: []authz.Action{authz.ActionCreate, authz.ActionUpdate, authz.ActionDelete}, allow: false}, + + // another resource type + {resource: authz.ResourceProject.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.Org(defOrg).Owner("other"), actions: authz.AllActions(), allow: false}, + + // Resources in default org + {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceProject.Org(defOrg), actions: authz.AllActions(), allow: false}, + + // Objs in other orgs + {resource: authz.ResourceWorkspace.Org("other"), actions: authz.AllActions(), allow: false}, + // Obj in other org owned by me + {resource: authz.ResourceProject.Org("other").Owner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceProject.Org("other").Owner(user.ID()).AsID(defWorkspaceID), actions: authz.AllActions(), allow: false}, + + // Site wide + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + }) + + // In practice this is a token scope on a regular subject + user = authz.SubjectTODO{ + UserID: "me", + Site: []authz.Role{}, + Org: map[string][]authz.Role{ + defOrg: {authz.RoleReadOnly}, + }, + User: []authz.Role{authz.RoleReadOnly}, + } + + testAuthorize(t, "ReadOnly", user, []authTestCase{ + // Read + {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: []authz.Action{authz.ActionRead}, allow: true}, + {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: []authz.Action{authz.ActionRead}, allow: true}, + {resource: authz.ResourceWorkspace.Org(defOrg), actions: []authz.Action{authz.ActionRead}, allow: true}, + {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: []authz.Action{authz.ActionRead}, allow: true}, + {resource: authz.ResourceWorkspace, actions: []authz.Action{authz.ActionRead}, allow: false}, + + // Other + {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + }) +} + +type authTestCase struct { + resource authz.Resource + actions []authz.Action + allow bool +} + +func testAuthorize(t *testing.T, name string, subject authz.Subject, cases []authTestCase) { + for _, c := range cases { + t.Run(name, func(t *testing.T) { + for _, a := range c.actions { + err := authz.Authorize(subject, c.resource, a) + if c.allow { + require.NoError(t, err, "expected no error for testcase action %s", a) + continue + } + require.Error(t, err, "expected unauthorized") + } + }) + } +} diff --git a/coderd/authz/role.go b/coderd/authz/role.go index cb8b27812d5cb..9050d7dcdc48f 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -5,23 +5,41 @@ type Role []Permission var ( RoleAllowAll = Role{ { - Negate: false, - OrganizationID: "*", - ResourceType: "*", - ResourceID: "*", - Action: "*", + Negate: false, + ResourceType: "*", + ResourceID: "*", + Action: "*", }, } RoleReadOnly = Role{ { - Negate: false, - OrganizationID: "*", - ResourceType: "*", - ResourceID: "*", - Action: ActionRead, + Negate: false, + ResourceType: "*", + ResourceID: "*", + Action: ActionRead, + }, + } + + RoleBlockAll = Role{ + { + Negate: true, + ResourceType: "*", + ResourceID: "*", + Action: "*", }, } RoleNoPerm = Role{} ) + +func WorkspaceAgentRole(workspaceID string) Role { + return Role{ + { + Negate: false, + ResourceType: ResourceWorkspace, + ResourceID: workspaceID, + Action: ActionRead, + }, + } +} diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index f1e48762e598d..0d50f5f44013a 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -13,7 +13,7 @@ type Subject interface { // 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) + OrgRoles(ctx context.Context, orgID string) ([]Role, bool, error) UserRoles() ([]Role, error) } @@ -36,8 +36,9 @@ func (s SubjectTODO) SiteRoles() ([]Role, error) { return s.Site, nil } -func (s SubjectTODO) OrgRoles(_ context.Context, orgID string) ([]Role, error) { - return s.Org[orgID], nil +func (s SubjectTODO) OrgRoles(_ context.Context, orgID string) ([]Role, bool, error) { + v, ok := s.Org[orgID] + return v, ok, nil } func (s SubjectTODO) UserRoles() ([]Role, error) { From 22cf0cce3978e6962aff68d3caed2bedee19cced Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 8 Apr 2022 15:05:09 +0100 Subject: [PATCH 45/75] fix: made roles named and now they span all levels --- coderd/authz/authz.go | 2 +- coderd/authz/authz_test.go | 105 +++++++++++------------------------ coderd/authz/example_test.go | 3 +- coderd/authz/permission.go | 6 +- coderd/authz/role.go | 92 ++++++++++++++++++++++-------- coderd/authz/subject.go | 26 ++------- 6 files changed, 108 insertions(+), 126 deletions(-) diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index 6b58c08ab34bc..7291abddf8cca 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -2,7 +2,7 @@ package authz import "errors" -var ErrUnauthorized = errors.New("unauthorized") +var ErrUnauthorized = xerrors.New("unauthorized") // TODO: Implement Authorize func Authorize(subj Subject, obj Resource, action Action) error { diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index f18369d19c693..4fdf21ca65287 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -8,67 +8,16 @@ import ( "github.com/coder/coder/coderd/authz" ) -func TestAuthorize(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - subject authz.Subject - resource authz.Resource - actions []authz.Action - error string - }{ - { - name: "unauthenticated user cannot perform an action", - subject: authz.SubjectTODO{ - UserID: "", - Site: []authz.Role{authz.RoleNoPerm}, - }, - resource: authz.ResourceWorkspace, - actions: []authz.Action{authz.ActionRead, authz.ActionCreate, authz.ActionDelete, authz.ActionUpdate}, - error: "unauthorized", - }, - { - name: "admin can do anything", - subject: authz.SubjectTODO{ - UserID: "admin", - Site: []authz.Role{authz.RoleAllowAll}, - }, - resource: authz.ResourceWorkspace, - actions: []authz.Action{authz.ActionRead, authz.ActionCreate, authz.ActionDelete, authz.ActionUpdate}, - error: "", - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - for _, action := range testCase.actions { - err := authz.Authorize(testCase.subject, testCase.resource, action) - if testCase.error == "" { - require.NoError(t, err, "expected no error for testcase testcase %q action %s", testCase.name, action) - continue - } - require.EqualError(t, err, testCase.error, "unexpected error") - } - }) - } -} - // TestAuthorizeBasic test the very basic roles that are commonly used. func TestAuthorizeBasic(t *testing.T) { + t.Skip("TODO: unskip when rego is done") t.Parallel() defOrg := "default" defWorkspaceID := "1234" user := authz.SubjectTODO{ UserID: "me", - Site: []authz.Role{}, - Org: map[string][]authz.Role{ - defOrg: {}, - }, - User: []authz.Role{authz.RoleAllowAll}, + Roles: []authz.Role{authz.RoleSiteMember, authz.OrgMember(defOrg)}, } testAuthorize(t, "Member", user, []authTestCase{ @@ -94,11 +43,7 @@ func TestAuthorizeBasic(t *testing.T) { user = authz.SubjectTODO{ UserID: "me", - Site: []authz.Role{authz.RoleBlockAll}, - Org: map[string][]authz.Role{ - defOrg: {}, - }, - User: []authz.Role{authz.RoleAllowAll}, + Roles: []authz.Role{authz.RoleDenyAll}, } testAuthorize(t, "DeletedMember", user, []authTestCase{ @@ -123,11 +68,10 @@ func TestAuthorizeBasic(t *testing.T) { user = authz.SubjectTODO{ UserID: "me", - Site: []authz.Role{}, - Org: map[string][]authz.Role{ - defOrg: {authz.RoleAllowAll}, + Roles: []authz.Role{ + authz.OrgAdmin(defOrg), + authz.RoleSiteMember, }, - User: []authz.Role{authz.RoleAllowAll}, } testAuthorize(t, "OrgAdmin", user, []authTestCase{ @@ -154,9 +98,10 @@ func TestAuthorizeBasic(t *testing.T) { user = authz.SubjectTODO{ UserID: "me", - Site: []authz.Role{authz.RoleAllowAll}, - Org: map[string][]authz.Role{}, - User: []authz.Role{authz.RoleAllowAll}, + Roles: []authz.Role{ + authz.RoleSiteAdmin, + authz.RoleSiteMember, + }, } testAuthorize(t, "SiteAdmin", user, []authTestCase{ @@ -184,11 +129,9 @@ func TestAuthorizeBasic(t *testing.T) { // In practice this is a token scope on a regular subject user = authz.SubjectTODO{ UserID: "me", - Site: []authz.Role{}, - Org: map[string][]authz.Role{ - defOrg: {}, + Roles: []authz.Role{ + authz.WorkspaceAgentRole(defWorkspaceID), }, - User: []authz.Role{authz.WorkspaceAgentRole(defWorkspaceID)}, } testAuthorize(t, "WorkspaceAgentToken", user, []authTestCase{ @@ -221,11 +164,27 @@ func TestAuthorizeBasic(t *testing.T) { // In practice this is a token scope on a regular subject user = authz.SubjectTODO{ UserID: "me", - Site: []authz.Role{}, - Org: map[string][]authz.Role{ - defOrg: {authz.RoleReadOnly}, + Roles: []authz.Role{ + { + Site: []authz.Permission{}, + Org: map[string][]authz.Permission{ + defOrg: {{ + Negate: false, + ResourceType: "*", + ResourceID: "*", + Action: authz.ActionRead, + }}, + }, + User: []authz.Permission{ + { + Negate: false, + ResourceType: "*", + ResourceID: "*", + Action: authz.ActionRead, + }, + }, + }, }, - User: []authz.Role{authz.RoleReadOnly}, } testAuthorize(t, "ReadOnly", user, []authTestCase{ diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index 5e25fde9e9614..24d4a89592004 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -11,13 +11,14 @@ import ( // TestExample gives some examples on how to use the authz library. // This serves to test syntax more than functionality. func TestExample(t *testing.T) { + t.Skip("TODO: unskip when rego is done") 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", - Site: []authz.Role{authz.RoleReadOnly}, + Roles: []authz.Role{authz.RoleSiteAdmin}, } // TODO: Uncomment all assertions when implementation is done. diff --git a/coderd/authz/permission.go b/coderd/authz/permission.go index 0147f2f583542..bb8ffdb3452b0 100644 --- a/coderd/authz/permission.go +++ b/coderd/authz/permission.go @@ -2,11 +2,7 @@ package authz type Permission struct { // Negate makes this a negative permission - Negate bool - // OrganizationID is used for identifying a particular org. - // org:1234 - OrganizationID string - + Negate bool ResourceType ResourceType ResourceID string Action Action diff --git a/coderd/authz/role.go b/coderd/authz/role.go index 9050d7dcdc48f..68da2a4aecda6 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -1,45 +1,89 @@ package authz -type Role []Permission +import "fmt" + +type Role struct { + Name string + Site []Permission + Org map[string][]Permission + User []Permission +} var ( - RoleAllowAll = Role{ - { - Negate: false, - ResourceType: "*", - ResourceID: "*", - Action: "*", + RoleSiteAdmin = Role{ + Name: "site-admin", + Site: []Permission{ + { + Negate: false, + ResourceType: "*", + ResourceID: "*", + Action: "*", + }, }, } - RoleReadOnly = Role{ - { - Negate: false, - ResourceType: "*", - ResourceID: "*", - Action: ActionRead, + RoleDenyAll = Role{ + Name: "deny-all", + Site: []Permission{ + { + Negate: true, + ResourceType: "*", + ResourceID: "*", + Action: "*", + }, }, } - RoleBlockAll = Role{ - { - Negate: true, - ResourceType: "*", - ResourceID: "*", - Action: "*", + RoleSiteMember = Role{ + Name: "site-member", + User: []Permission{ + { + Negate: false, + ResourceType: "*", + ResourceID: "*", + Action: "*", + }, }, } RoleNoPerm = Role{} ) +func OrgAdmin(orgID string) Role { + return Role{ + Name: "org-admin-" + orgID, + Org: map[string][]Permission{ + orgID: { + { + Negate: false, + ResourceType: "*", + ResourceID: "*", + Action: "*", + }, + }, + }, + } +} + +func OrgMember(orgID string) Role { + return Role{ + Name: "org-member-" + orgID, + Org: map[string][]Permission{ + orgID: {}, + }, + } +} + func WorkspaceAgentRole(workspaceID string) Role { return Role{ - { - Negate: false, - ResourceType: ResourceWorkspace, - ResourceID: workspaceID, - Action: ActionRead, + Name: fmt.Sprintf("agent-%s", workspaceID), + Site: []Permission{ + { + Negate: false, + ResourceType: ResourceWorkspace, + ResourceID: workspaceID, + Action: ActionRead, + }, }, } } diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index 0d50f5f44013a..11ef8846c6a4f 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -1,7 +1,5 @@ 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 { @@ -9,12 +7,7 @@ type Subject interface { // 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, bool, error) - UserRoles() ([]Role, error) + GetRoles() ([]Role, error) } // SubjectTODO is a placeholder until we get an actual actor struct in place. @@ -23,26 +16,15 @@ type Subject interface { type SubjectTODO struct { UserID string `json:"user_id"` - Site []Role - Org map[string][]Role - User []Role + Roles []Role } func (s SubjectTODO) ID() string { return s.UserID } -func (s SubjectTODO) SiteRoles() ([]Role, error) { - return s.Site, nil -} - -func (s SubjectTODO) OrgRoles(_ context.Context, orgID string) ([]Role, bool, error) { - v, ok := s.Org[orgID] - return v, ok, nil -} - -func (s SubjectTODO) UserRoles() ([]Role, error) { - return s.User, nil +func (s SubjectTODO) GetRoles() ([]Role, error) { + return s.Roles, nil } func (SubjectTODO) Scopes() ([]Permission, error) { From c51ddd1c625f7f78f3855c7fda82687c2e03a29a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 8 Apr 2022 15:06:25 +0100 Subject: [PATCH 46/75] fixup! fix: made roles named and now they span all levels --- coderd/authz/authz.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index 7291abddf8cca..fb3604e0da8a1 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -1,6 +1,6 @@ package authz -import "errors" +import "golang.org/x/xerrors" var ErrUnauthorized = xerrors.New("unauthorized") From 891e4424cc82ec8edb99471931b05ad33a9e82c5 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 8 Apr 2022 15:14:31 +0100 Subject: [PATCH 47/75] fixup! fixup! fix: made roles named and now they span all levels --- coderd/authz/role.go | 23 ++++++++++++++++++----- coderd/authz/subject.go | 9 ++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/coderd/authz/role.go b/coderd/authz/role.go index 68da2a4aecda6..7506e77912bf2 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -2,6 +2,12 @@ package authz import "fmt" +// Role is a set of permissions at multiple levels: +// - Site level permissions apply EVERYWHERE +// - Org level permissions apply to EVERYTHING in a given ORG +// - User level permissions are the lowest +// In most cases, you will just want to use the pre-defined roles +// below. type Role struct { Name string Site []Permission @@ -10,6 +16,7 @@ type Role struct { } var ( + // RoleSiteAdmin is a role that allows everything everywhere. RoleSiteAdmin = Role{ Name: "site-admin", Site: []Permission{ @@ -22,6 +29,7 @@ var ( }, } + // RoleDenyAll is a role that denies everything everywhere. RoleDenyAll = Role{ Name: "deny-all", Site: []Permission{ @@ -34,6 +42,7 @@ var ( }, } + // RoleSiteMember is a role that allows access to user-level resources. RoleSiteMember = Role{ Name: "site-member", User: []Permission{ @@ -45,11 +54,11 @@ var ( }, }, } - - RoleNoPerm = Role{} ) -func OrgAdmin(orgID string) Role { +// RoleOrgAdmin returns a role with all actions allows in a given +// organization scope. +func RoleOrgAdmin(orgID string) Role { return Role{ Name: "org-admin-" + orgID, Org: map[string][]Permission{ @@ -65,7 +74,9 @@ func OrgAdmin(orgID string) Role { } } -func OrgMember(orgID string) Role { +// RoleOrgMember returns a role with default permissions in a given +// organization scope. +func RoleOrgMember(orgID string) Role { return Role{ Name: "org-member-" + orgID, Org: map[string][]Permission{ @@ -74,7 +85,9 @@ func OrgMember(orgID string) Role { } } -func WorkspaceAgentRole(workspaceID string) Role { +// RoleWorkspaceAgent returns a role with permission to read a given +// workspace. +func RoleWorkspaceAgent(workspaceID string) Role { return Role{ Name: fmt.Sprintf("agent-%s", workspaceID), Site: []Permission{ diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go index 11ef8846c6a4f..4d97280be3f8d 100644 --- a/coderd/authz/subject.go +++ b/coderd/authz/subject.go @@ -6,7 +6,7 @@ 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 - + // GetRoles returns the list of roles for the given GetRoles() ([]Role, error) } @@ -15,8 +15,7 @@ type Subject interface { // TODO: @emyrk delete this data structure when authn exists type SubjectTODO struct { UserID string `json:"user_id"` - - Roles []Role + Roles []Role } func (s SubjectTODO) ID() string { @@ -26,7 +25,3 @@ func (s SubjectTODO) ID() string { func (s SubjectTODO) GetRoles() ([]Role, error) { return s.Roles, nil } - -func (SubjectTODO) Scopes() ([]Permission, error) { - return []Permission{}, nil -} From ad048dd2fdddda7ab594120831221743645b8340 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 8 Apr 2022 15:17:14 +0100 Subject: [PATCH 48/75] vscode is illiterate --- coderd/authz/authz_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 4fdf21ca65287..6c9696332dbd6 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -69,7 +69,7 @@ func TestAuthorizeBasic(t *testing.T) { user = authz.SubjectTODO{ UserID: "me", Roles: []authz.Role{ - authz.OrgAdmin(defOrg), + authz.RoleOrgAdmin(defOrg), authz.RoleSiteMember, }, } @@ -130,7 +130,7 @@ func TestAuthorizeBasic(t *testing.T) { user = authz.SubjectTODO{ UserID: "me", Roles: []authz.Role{ - authz.WorkspaceAgentRole(defWorkspaceID), + authz.RoleWorkspaceAgent(defWorkspaceID), }, } From bb28930ceb10f95cfe96c83c23a8a7444419f360 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 8 Apr 2022 15:21:50 +0100 Subject: [PATCH 49/75] fixup! vscode is illiterate --- coderd/authz/authz_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 6c9696332dbd6..e6741d7dc1816 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -17,7 +17,7 @@ func TestAuthorizeBasic(t *testing.T) { user := authz.SubjectTODO{ UserID: "me", - Roles: []authz.Role{authz.RoleSiteMember, authz.OrgMember(defOrg)}, + Roles: []authz.Role{authz.RoleSiteMember, authz.RoleOrgMember(defOrg)}, } testAuthorize(t, "Member", user, []authTestCase{ From 2e23a34b456f8e02a0187090a5dc09aa9a8ce3b3 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 8 Apr 2022 09:51:40 -0500 Subject: [PATCH 50/75] Remove Org & Owner obj interfaces Will likely bring them back, just removing unused code --- coderd/authz/authz.go | 39 ++++++++++++++++++++++----- coderd/authz/example_test.go | 5 +++- coderd/authz/object.go | 9 ------- coderd/authz/resources.go | 5 ++++ coderd/authz/role.go | 52 +++++++++++++++++++++++------------- 5 files changed, 75 insertions(+), 35 deletions(-) diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index fb3604e0da8a1..51e3ebb44ad8f 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -7,12 +7,37 @@ var ErrUnauthorized = xerrors.New("unauthorized") // TODO: Implement Authorize 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) -} + var _, _, _ = subj, obj, action + roles, err := subj.GetRoles() + if err != nil { + return ErrUnauthorized + } + + // Merge before we send to rego to optimize the json payload. + // TODO: Benchmark the rego, it might be ok to just send everything and let + // rego do the merges. The number of roles will be small, so it might not + // matter. This code exists just to show how you can merge the roles + // into a single one for evaluation if need be. + // If done in rego, the roles will not be merged, and just walked over + // 1 by 1. + var merged Role + for _, r := range roles { + merged.Site = append(merged.Site, r.Site...) + // Only grab user roles if the resource is owned by a user. + // These roles only apply if the subject is said owner. + if obj.OwnerID() != "" && obj.OwnerID() == subj.ID() { + merged.User = append(merged.User, r.User...) + } + + // Grab org roles if the resource is owned by a given organization. + if obj.OrgOwnerID() != "" { + orgID := obj.OrgOwnerID() + if v, ok := r.Org[orgID]; ok { + merged.Org[orgID] = append(merged.Org[orgID], v...) + } + } + } -// AuthorizePermissions runs the authorize function with the raw permissions in a single list. -func AuthorizePermissions(_ string, _ []Permission, _ Resource, _ Action) error { - // return nil - // for now, nothing is allowed - return ErrUnauthorized + // TODO: Send to rego policy evaluation. + return nil } diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index 24d4a89592004..6c0e0f712df88 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -18,7 +18,10 @@ func TestExample(t *testing.T) { // fulfills the interface. Until then, use a placeholder. user := authz.SubjectTODO{ UserID: "alice", - Roles: []authz.Role{authz.RoleSiteAdmin}, + Roles: []authz.Role{ + authz.RoleOrgAdmin("default"), + authz.RoleSiteMember, + }, } // TODO: Uncomment all assertions when implementation is done. diff --git a/coderd/authz/object.go b/coderd/authz/object.go index 2c26e9e768bb3..9eb630166a6bd 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -3,21 +3,12 @@ package authz type Resource interface { ID() string ResourceType() ResourceType -} -type UserResource interface { - Resource OwnerID() string -} - -type OrgResource interface { - Resource 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. diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index dd519838415a6..8726ea52bd851 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -8,6 +8,7 @@ const ( ResourceProject ResourceType = "project" ResourceDevURL ResourceType = "devurl" ResourceUser ResourceType = "user" + ResourceAuditLogs ResourceType = "audit-logs" ) func (ResourceType) ID() string { @@ -18,6 +19,9 @@ func (t ResourceType) ResourceType() ResourceType { return t } +func (ResourceType) OwnerID() string { return "" } +func (ResourceType) OrgOwnerID() string { return "" } + // Org adds an org OwnerID to the resource //nolint:revive func (r ResourceType) Org(orgID string) zObject { @@ -36,6 +40,7 @@ func (r ResourceType) Owner(id string) zObject { } } +// AsID adds a resource ID to the resource //nolint:revive func (r ResourceType) AsID(id string) zObject { return zObject{ diff --git a/coderd/authz/role.go b/coderd/authz/role.go index 7506e77912bf2..0bb41910caf15 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -2,6 +2,8 @@ package authz import "fmt" +const wildcard = "*" + // Role is a set of permissions at multiple levels: // - Site level permissions apply EVERYWHERE // - Org level permissions apply to EVERYTHING in a given ORG @@ -15,6 +17,8 @@ type Role struct { User []Permission } +// Roles are stored as structs, so they can be serialized and stored. Until we store them elsewhere, +// const's will do just fine. var ( // RoleSiteAdmin is a role that allows everything everywhere. RoleSiteAdmin = Role{ @@ -22,35 +26,47 @@ var ( Site: []Permission{ { Negate: false, - ResourceType: "*", - ResourceID: "*", - Action: "*", + ResourceType: wildcard, + ResourceID: wildcard, + Action: wildcard, }, }, } - // RoleDenyAll is a role that denies everything everywhere. - RoleDenyAll = Role{ - Name: "deny-all", - Site: []Permission{ + // RoleSiteMember is a role that allows access to user-level resources. + RoleSiteMember = Role{ + Name: "site-member", + User: []Permission{ { - Negate: true, - ResourceType: "*", - ResourceID: "*", - Action: "*", + Negate: false, + ResourceType: wildcard, + ResourceID: wildcard, + Action: wildcard, }, }, } - // RoleSiteMember is a role that allows access to user-level resources. - RoleSiteMember = Role{ - Name: "site-member", - User: []Permission{ + RoleSiteAuditor = Role{ + Name: "site-auditor", + Site: []Permission{ { Negate: false, - ResourceType: "*", - ResourceID: "*", - Action: "*", + ResourceType: ResourceAuditLogs, + ResourceID: wildcard, + Action: ActionRead, + }, + }, + } + + // RoleDenyAll is a role that denies everything everywhere. + RoleDenyAll = Role{ + Name: "deny-all", + Site: []Permission{ + { + Negate: true, + ResourceType: wildcard, + ResourceID: wildcard, + Action: wildcard, }, }, } From 13466e1b0c831ecf5a39f6dce66ad8095902cd06 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 8 Apr 2022 10:00:51 -0500 Subject: [PATCH 51/75] Show a nice builder syntax --- coderd/authz/role.go | 54 ++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/coderd/authz/role.go b/coderd/authz/role.go index 0bb41910caf15..07d803e20346b 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -23,44 +23,34 @@ var ( // RoleSiteAdmin is a role that allows everything everywhere. RoleSiteAdmin = Role{ Name: "site-admin", - Site: []Permission{ - { - Negate: false, - ResourceType: wildcard, - ResourceID: wildcard, - Action: wildcard, - }, - }, + Site: permissions(map[ResourceType][]Action{ + wildcard: {wildcard}, + }), } // RoleSiteMember is a role that allows access to user-level resources. RoleSiteMember = Role{ Name: "site-member", - User: []Permission{ - { - Negate: false, - ResourceType: wildcard, - ResourceID: wildcard, - Action: wildcard, - }, - }, + User: permissions(map[ResourceType][]Action{ + wildcard: {wildcard}, + }), } + // RoleSiteAuditor is an example on how to give more precise permissions RoleSiteAuditor = Role{ Name: "site-auditor", - Site: []Permission{ - { - Negate: false, - ResourceType: ResourceAuditLogs, - ResourceID: wildcard, - Action: ActionRead, - }, - }, + Site: permissions(map[ResourceType][]Action{ + ResourceAuditLogs: {ActionRead}, + // Should be able to read user details to associate with logs. + // Without this the user-id in logs is not very helpful + ResourceUser: {ActionRead}, + }), } // RoleDenyAll is a role that denies everything everywhere. RoleDenyAll = Role{ Name: "deny-all", + // List out deny permissions explicitly Site: []Permission{ { Negate: true, @@ -116,3 +106,19 @@ func RoleWorkspaceAgent(workspaceID string) Role { }, } } + +func permissions(perms map[ResourceType][]Action) []Permission { + list := make([]Permission, 0, len(perms)) + for k, actions := range perms { + for _, act := range actions { + act := act + list = append(list, Permission{ + Negate: false, + ResourceType: k, + ResourceID: wildcard, + Action: act, + }) + } + } + return list +} From 3ac2eaaf7369b63e8e3b063c7512711919a66a12 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 8 Apr 2022 11:23:05 -0500 Subject: [PATCH 52/75] Add more tabled tests --- coderd/authz/authz_test.go | 547 +++++++++++++++++++++++++++-------- coderd/authz/example_test.go | 6 +- coderd/authz/object.go | 10 +- coderd/authz/resources.go | 12 +- coderd/authz/role.go | 30 +- 5 files changed, 466 insertions(+), 139 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index e6741d7dc1816..ae1f001d0e45e 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -8,12 +8,12 @@ import ( "github.com/coder/coder/coderd/authz" ) -// TestAuthorizeBasic test the very basic roles that are commonly used. -func TestAuthorizeBasic(t *testing.T) { +// TestAuthorizeDomain test the very basic roles that are commonly used. +func TestAuthorizeDomain(t *testing.T) { t.Skip("TODO: unskip when rego is done") t.Parallel() defOrg := "default" - defWorkspaceID := "1234" + wrkID := "1234" user := authz.SubjectTODO{ UserID: "me", @@ -21,24 +21,42 @@ func TestAuthorizeBasic(t *testing.T) { } testAuthorize(t, "Member", user, []authTestCase{ - // Read my own resources - {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: true}, - // My workspace in my org - {resource: authz.ResourceProject.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceProject.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: authz.AllActions(), allow: true}, - - // Read resources in default org - {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceProject.Org(defOrg), actions: authz.AllActions(), allow: true}, - - // Objs in other orgs - {resource: authz.ResourceWorkspace.Org("other"), actions: authz.AllActions(), allow: false}, - // Obj in other org owned by me - {resource: authz.ResourceProject.Org("other").Owner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceProject.Org("other").Owner(user.ID()).AsID(defWorkspaceID), actions: authz.AllActions(), allow: false}, - - // Site wide + // Org + me + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, + + {resource: authz.ResourceWorkspace.SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + + // Other org + me + id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + + // Other org + other user + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + // Other org + other use + other id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetID("not-id"), actions: authz.AllActions(), allow: false}, }) user = authz.SubjectTODO{ @@ -47,23 +65,42 @@ func TestAuthorizeBasic(t *testing.T) { } testAuthorize(t, "DeletedMember", user, []authTestCase{ - // Read my own resources - {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: false}, - // My workspace in my org - {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: false}, - - // Read resources in default org - {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: false}, - - // Objs in other orgs - {resource: authz.ResourceWorkspace.Org("other"), actions: authz.AllActions(), allow: false}, - // Obj in other org owned by me - {resource: authz.ResourceProject.Org("other").Owner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceProject.Org("other").Owner(user.ID()).AsID("1234"), actions: authz.AllActions(), allow: false}, - - // Site wide + // Org + me + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + + // Other org + me + id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + + // Other org + other user + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + // Other org + other use + other id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetID("not-id"), actions: authz.AllActions(), allow: false}, }) user = authz.SubjectTODO{ @@ -75,25 +112,42 @@ func TestAuthorizeBasic(t *testing.T) { } testAuthorize(t, "OrgAdmin", user, []authTestCase{ - // Read my own resources - {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: true}, - // My workspace in my org - {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: true}, - // Another workspace in my org - {resource: authz.ResourceWorkspace.Org(defOrg).Owner("other"), actions: authz.AllActions(), allow: true}, - - // Read resources in default org - {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceProject.Org(defOrg), actions: authz.AllActions(), allow: true}, - - // Objs in other orgs - {resource: authz.ResourceWorkspace.Org("other"), actions: authz.AllActions(), allow: false}, - // Obj in other org owned by me - {resource: authz.ResourceProject.Org("other").Owner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceProject.Org("other").Owner(user.ID()).AsID("1234"), actions: authz.AllActions(), allow: false}, - - // Site wide + // Org + me + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: authz.AllActions(), allow: true}, + + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, + + {resource: authz.ResourceWorkspace.SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + + // Other org + me + id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + + // Other org + other user + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: authz.AllActions(), allow: true}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + // Other org + other use + other id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetID("not-id"), actions: authz.AllActions(), allow: false}, }) user = authz.SubjectTODO{ @@ -105,60 +159,89 @@ func TestAuthorizeBasic(t *testing.T) { } testAuthorize(t, "SiteAdmin", user, []authTestCase{ - // Read my own resources - {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: true}, - // My workspace in my org - {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: true}, - // Another workspace in my org - {resource: authz.ResourceWorkspace.Org(defOrg).Owner("other"), actions: authz.AllActions(), allow: true}, - - // Read resources in default org - {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceProject.Org(defOrg), actions: authz.AllActions(), allow: true}, - - // Objs in other orgs - {resource: authz.ResourceWorkspace.Org("other"), actions: authz.AllActions(), allow: true}, - // Obj in other org owned by me - {resource: authz.ResourceProject.Org("other").Owner(user.ID()), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceProject.Org("other").Owner(user.ID()).AsID("1234"), actions: authz.AllActions(), allow: true}, - - // Site wide + // Org + me + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: authz.AllActions(), allow: true}, + + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, + + {resource: authz.ResourceWorkspace.SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: true}, + + // Other org + me + id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: true}, + + // Other org + other user + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: authz.AllActions(), allow: true}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: true}, + + // Other org + other use + other id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: true}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: true}, + + {resource: authz.ResourceWorkspace.SetID("not-id"), actions: authz.AllActions(), allow: true}, }) // In practice this is a token scope on a regular subject user = authz.SubjectTODO{ UserID: "me", Roles: []authz.Role{ - authz.RoleWorkspaceAgent(defWorkspaceID), + authz.RoleWorkspaceAgent(wrkID), }, } testAuthorize(t, "WorkspaceAgentToken", user, []authTestCase{ - // Read workspace by ID - {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: []authz.Action{authz.ActionRead}, allow: true}, - // C_UD - {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: []authz.Action{authz.ActionCreate, authz.ActionUpdate, authz.ActionDelete}, allow: false}, + // Org + me + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: authz.AllActions(), allow: false}, - // another resource type - {resource: authz.ResourceProject.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.Org(defOrg).Owner("other"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetID(wrkID), actions: authz.AllActions(), allow: true}, - // Resources in default org - {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceProject.Org(defOrg), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, - // Objs in other orgs - {resource: authz.ResourceWorkspace.Org("other"), actions: authz.AllActions(), allow: false}, - // Obj in other org owned by me - {resource: authz.ResourceProject.Org("other").Owner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceProject.Org("other").Owner(user.ID()).AsID(defWorkspaceID), actions: authz.AllActions(), allow: false}, + // Other org + me + id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, - // Site wide - {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + // Other org + other user + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + // Other org + other use + other id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + + {resource: authz.ResourceWorkspace.SetID("not-id"), actions: authz.AllActions(), allow: false}, }) // In practice this is a token scope on a regular subject @@ -187,21 +270,247 @@ func TestAuthorizeBasic(t *testing.T) { }, } - testAuthorize(t, "ReadOnly", user, []authTestCase{ - // Read - {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: []authz.Action{authz.ActionRead}, allow: true}, - {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: []authz.Action{authz.ActionRead}, allow: true}, - {resource: authz.ResourceWorkspace.Org(defOrg), actions: []authz.Action{authz.ActionRead}, allow: true}, - {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: []authz.Action{authz.ActionRead}, allow: true}, - {resource: authz.ResourceWorkspace, actions: []authz.Action{authz.ActionRead}, allow: false}, - - // Other - {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()).AsID(defWorkspaceID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.Org(defOrg).Owner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.Org(defOrg), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.Owner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, - }) + testAuthorize(t, "ReadOnly", user, + cases(func(c authTestCase) authTestCase { + c.actions = []authz.Action{authz.ActionRead} + return c + }, []authTestCase{ + // Read + // Org + me + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), allow: true}, + + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), allow: true}, + + {resource: authz.ResourceWorkspace.SetID(wrkID), allow: false}, + + {resource: authz.ResourceWorkspace, allow: false}, + + // Other org + me + id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), allow: false}, + + // Other org + other user + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), allow: true}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), allow: false}, + + // Other org + other use + other id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), allow: false}, + + {resource: authz.ResourceWorkspace.SetID("not-id"), allow: false}, + }), + + // Pass non-read actions + cases(func(c authTestCase) authTestCase { + c.actions = []authz.Action{authz.ActionCreate, authz.ActionUpdate, authz.ActionDelete} + c.allow = false + return c + }, []authTestCase{ + // Read + // Org + me + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID())}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg)}, + + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID())}, + + {resource: authz.ResourceWorkspace.SetID(wrkID)}, + + {resource: authz.ResourceWorkspace}, + + // Other org + me + id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID())}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOrg("other")}, + + // Other org + other user + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me")}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOwner("not-me")}, + + // Other org + other use + other id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id")}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me")}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id")}, + {resource: authz.ResourceWorkspace.SetOrg("other")}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id")}, + {resource: authz.ResourceWorkspace.SetOwner("not-me")}, + + {resource: authz.ResourceWorkspace.SetID("not-id")}, + })) +} + +// TestAuthorizeLevels ensures level overrides are acting appropriately +func TestAuthorizeLevels(t *testing.T) { + defOrg := "default" + wrkID := "1234" + + user := authz.SubjectTODO{ + UserID: "me", + Roles: []authz.Role{ + authz.RoleSiteAdmin, + authz.RoleOrgDenyAll(defOrg), + { + Name: "user-deny-all", + // List out deny permissions explicitly + Site: []authz.Permission{ + { + Negate: true, + ResourceType: authz.Wildcard, + ResourceID: authz.Wildcard, + Action: authz.Wildcard, + }, + }, + }, + }, + } + + testAuthorize(t, "AdminAlwaysAllow", user, + cases(func(c authTestCase) authTestCase { + c.actions = authz.AllActions() + c.allow = true + return c + }, []authTestCase{ + // Org + me + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID())}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg)}, + + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID())}, + + {resource: authz.ResourceWorkspace.SetID(wrkID)}, + + {resource: authz.ResourceWorkspace}, + + // Other org + me + id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID())}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOrg("other")}, + + // Other org + other user + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me")}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID)}, + {resource: authz.ResourceWorkspace.SetOwner("not-me")}, + + // Other org + other use + other id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id")}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me")}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id")}, + {resource: authz.ResourceWorkspace.SetOrg("other")}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id")}, + {resource: authz.ResourceWorkspace.SetOwner("not-me")}, + + {resource: authz.ResourceWorkspace.SetID("not-id")}, + })) + + user = authz.SubjectTODO{ + UserID: "me", + Roles: []authz.Role{ + { + Name: "site-noise", + Site: []authz.Permission{ + { + Negate: true, + ResourceType: "random", + ResourceID: authz.Wildcard, + Action: authz.Wildcard, + }, + }, + }, + authz.RoleOrgAdmin(defOrg), + { + Name: "user-deny-all", + // List out deny permissions explicitly + Site: []authz.Permission{ + { + Negate: true, + ResourceType: authz.Wildcard, + ResourceID: authz.Wildcard, + Action: authz.Wildcard, + }, + }, + }, + }, + } + + testAuthorize(t, "OrgAllowAll", user, + cases(func(c authTestCase) authTestCase { + c.actions = authz.AllActions() + return c + }, []authTestCase{ + // Org + me + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), allow: true}, + + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), allow: false}, + + {resource: authz.ResourceWorkspace.SetID(wrkID), allow: false}, + + {resource: authz.ResourceWorkspace, allow: false}, + + // Other org + me + id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), allow: false}, + + // Other org + other user + id + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), allow: true}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), allow: false}, + + // Other org + other use + other id + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), allow: false}, + + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), allow: false}, + + {resource: authz.ResourceWorkspace.SetID("not-id"), allow: false}, + })) +} + +func cases(opt func(c authTestCase) authTestCase, cases []authTestCase) []authTestCase { + if opt == nil { + return cases + } + for i := range cases { + cases[i] = opt(cases[i]) + } + return cases } type authTestCase struct { @@ -210,17 +519,19 @@ type authTestCase struct { allow bool } -func testAuthorize(t *testing.T, name string, subject authz.Subject, cases []authTestCase) { - for _, c := range cases { - t.Run(name, func(t *testing.T) { - for _, a := range c.actions { - err := authz.Authorize(subject, c.resource, a) - if c.allow { - require.NoError(t, err, "expected no error for testcase action %s", a) - continue +func testAuthorize(t *testing.T, name string, subject authz.Subject, sets ...[]authTestCase) { + for _, cases := range sets { + for _, c := range cases { + t.Run(name, func(t *testing.T) { + for _, a := range c.actions { + err := authz.Authorize(subject, c.resource, a) + if c.allow { + require.NoError(t, err, "expected no error for testcase action %s", a) + continue + } + require.Error(t, err, "expected unauthorized") } - require.Error(t, err, "expected unauthorized") - } - }) + }) + } } } diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index 6c0e0f712df88..b7c96515e882b 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -37,17 +37,17 @@ func TestExample(t *testing.T) { //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) + err := authz.Authorize(user, authz.ResourceWorkspace.SetOrg("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) + err := authz.Authorize(user, authz.ResourceWorkspace.SetOrg("default").SetOwner(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) + err = authz.Authorize(user, authz.ResourceWorkspace.SetOrg("default").SetOwner(user.UserID).SetID("1234"), authz.ActionRead) require.NoError(t, err, "this user can read workspace '1234'") }) diff --git a/coderd/authz/object.go b/coderd/authz/object.go index 9eb630166a6bd..41a353a351c67 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -41,22 +41,22 @@ func (z zObject) OrgOwnerID() string { return z.orgOwner } -// Org adds an org OwnerID to the resource +// SetOrg adds an org OwnerID to the resource //nolint:revive -func (z zObject) Org(orgID string) zObject { +func (z zObject) SetOrg(orgID string) zObject { z.orgOwner = orgID return z } -// Owner adds an OwnerID to the resource +// SetOwner adds an OwnerID to the resource //nolint:revive -func (z zObject) Owner(id string) zObject { +func (z zObject) SetOwner(id string) zObject { z.owner = id return z } //nolint:revive -func (z zObject) AsID(id string) zObject { +func (z zObject) SetID(id string) zObject { z.id = id return z } diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index 8726ea52bd851..cf1b5a94f066a 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -22,27 +22,27 @@ func (t ResourceType) ResourceType() ResourceType { func (ResourceType) OwnerID() string { return "" } func (ResourceType) OrgOwnerID() string { return "" } -// Org adds an org OwnerID to the resource +// SetOrg adds an org OwnerID to the resource //nolint:revive -func (r ResourceType) Org(orgID string) zObject { +func (r ResourceType) SetOrg(orgID string) zObject { return zObject{ orgOwner: orgID, objectType: r, } } -// Owner adds an OwnerID to the resource +// SetOwner adds an OwnerID to the resource //nolint:revive -func (r ResourceType) Owner(id string) zObject { +func (r ResourceType) SetOwner(id string) zObject { return zObject{ owner: id, objectType: r, } } -// AsID adds a resource ID to the resource +// SetID adds a resource ID to the resource //nolint:revive -func (r ResourceType) AsID(id string) zObject { +func (r ResourceType) SetID(id string) zObject { return zObject{ id: id, objectType: r, diff --git a/coderd/authz/role.go b/coderd/authz/role.go index 07d803e20346b..7d26f6234eba3 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -2,7 +2,7 @@ package authz import "fmt" -const wildcard = "*" +const Wildcard = "*" // Role is a set of permissions at multiple levels: // - Site level permissions apply EVERYWHERE @@ -24,7 +24,7 @@ var ( RoleSiteAdmin = Role{ Name: "site-admin", Site: permissions(map[ResourceType][]Action{ - wildcard: {wildcard}, + Wildcard: {Wildcard}, }), } @@ -32,7 +32,7 @@ var ( RoleSiteMember = Role{ Name: "site-member", User: permissions(map[ResourceType][]Action{ - wildcard: {wildcard}, + Wildcard: {Wildcard}, }), } @@ -54,14 +54,30 @@ var ( Site: []Permission{ { Negate: true, - ResourceType: wildcard, - ResourceID: wildcard, - Action: wildcard, + ResourceType: Wildcard, + ResourceID: Wildcard, + Action: Wildcard, }, }, } ) +func RoleOrgDenyAll(orgID string) Role { + return Role{ + Name: "org-deny-" + orgID, + Org: map[string][]Permission{ + orgID: { + { + Negate: true, + ResourceType: "*", + ResourceID: "*", + Action: "*", + }, + }, + }, + } +} + // RoleOrgAdmin returns a role with all actions allows in a given // organization scope. func RoleOrgAdmin(orgID string) Role { @@ -115,7 +131,7 @@ func permissions(perms map[ResourceType][]Action) []Permission { list = append(list, Permission{ Negate: false, ResourceType: k, - ResourceID: wildcard, + ResourceID: Wildcard, Action: act, }) } From c7dc715772b1db7f7fa6ea03fb34e8512d9e6ee1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 8 Apr 2022 11:24:18 -0500 Subject: [PATCH 53/75] fixup! Add more tabled tests --- coderd/authz/authz_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index ae1f001d0e45e..e58858602e46a 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -503,6 +503,7 @@ func TestAuthorizeLevels(t *testing.T) { })) } +// cases applies a given function to all test cases. This makes generalities easier to create. func cases(opt func(c authTestCase) authTestCase, cases []authTestCase) []authTestCase { if opt == nil { return cases From f5d95efd25f49720f198fd366e3c51911ce13459 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 8 Apr 2022 11:51:04 -0500 Subject: [PATCH 54/75] correct comments --- coderd/authz/README.md | 4 +- coderd/authz/action.go | 4 - coderd/authz/authz.go | 5 +- coderd/authz/authz_test.go | 242 ++++++++++++++++++----------------- coderd/authz/example_test.go | 3 +- 5 files changed, 130 insertions(+), 128 deletions(-) diff --git a/coderd/authz/README.md b/coderd/authz/README.md index 9f8d5c70b0edd..fe8c162091ebe 100644 --- a/coderd/authz/README.md +++ b/coderd/authz/README.md @@ -43,8 +43,8 @@ This can be represented by the following truth table, where Y represents *positi **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`. +- `negated` can be either `+` or `-`. If it is omitted, sign is assumed to be `+`. +- `level` is either `site`, `org`, or `user`. - `object` is any valid resource type. - `id` is any valid UUID v4. - `action` is `create`, `read`, `modify`, or `delete`. diff --git a/coderd/authz/action.go b/coderd/authz/action.go index 518ab2bc2ce19..eb7360ef1d6c9 100644 --- a/coderd/authz/action.go +++ b/coderd/authz/action.go @@ -1,9 +1,5 @@ package authz -func AllActions() []Action { - return []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete} -} - // Action represents the allowed actions to be done on an object. type Action string diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index 51e3ebb44ad8f..5af34f35f2bce 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -4,7 +4,7 @@ import "golang.org/x/xerrors" var ErrUnauthorized = xerrors.New("unauthorized") -// TODO: Implement Authorize +// TODO: Implement Authorize. This will be implmented in mainly rego. func Authorize(subj Subject, obj Resource, action Action) error { // TODO: Expand subject roles into their permissions as appropriate. Apply scopes. var _, _, _ = subj, obj, action @@ -22,6 +22,9 @@ func Authorize(subj Subject, obj Resource, action Action) error { // 1 by 1. var merged Role for _, r := range roles { + // Site, Org, and User permissions exist on every role. Pull out only the permissions that + // are relevant to the object. + merged.Site = append(merged.Site, r.Site...) // Only grab user roles if the resource is owned by a user. // These roles only apply if the subject is said owner. diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index e58858602e46a..aaa393282f3da 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -3,9 +3,8 @@ package authz_test import ( "testing" - "github.com/stretchr/testify/require" - "github.com/coder/coder/coderd/authz" + "github.com/stretchr/testify/require" ) // TestAuthorizeDomain test the very basic roles that are commonly used. @@ -22,41 +21,41 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "Member", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace, actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetID("not-id"), actions: allActions(), allow: false}, }) user = authz.SubjectTODO{ @@ -66,41 +65,41 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "DeletedMember", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace, actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetID("not-id"), actions: allActions(), allow: false}, }) user = authz.SubjectTODO{ @@ -113,41 +112,41 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "OrgAdmin", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetID(wrkID), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace, actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetID("not-id"), actions: allActions(), allow: false}, }) user = authz.SubjectTODO{ @@ -160,41 +159,41 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "SiteAdmin", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace, actions: allActions(), allow: true}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: true}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: true}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetID("not-id"), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetID("not-id"), actions: allActions(), allow: true}, }) // In practice this is a token scope on a regular subject @@ -207,41 +206,41 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "WorkspaceAgentToken", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID(wrkID), actions: authz.AllActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace, actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace, actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: authz.AllActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: authz.AllActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID("not-id"), actions: authz.AllActions(), allow: false}, + {resource: authz.ResourceWorkspace.SetID("not-id"), actions: allActions(), allow: false}, }) // In practice this is a token scope on a regular subject @@ -387,7 +386,7 @@ func TestAuthorizeLevels(t *testing.T) { testAuthorize(t, "AdminAlwaysAllow", user, cases(func(c authTestCase) authTestCase { - c.actions = authz.AllActions() + c.actions = allActions() c.allow = true return c }, []authTestCase{ @@ -461,7 +460,7 @@ func TestAuthorizeLevels(t *testing.T) { testAuthorize(t, "OrgAllowAll", user, cases(func(c authTestCase) authTestCase { - c.actions = authz.AllActions() + c.actions = allActions() return c }, []authTestCase{ // Org + me + id @@ -536,3 +535,8 @@ func testAuthorize(t *testing.T, name string, subject authz.Subject, sets ...[]a } } } + +// allActions is a helper function to return all the possible actions types. +func allActions() []authz.Action { + return []authz.Action{authz.ActionCreate, authz.ActionRead, authz.ActionUpdate, authz.ActionDelete} +} diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index b7c96515e882b..e61fa79ff1530 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -3,9 +3,8 @@ package authz_test import ( "testing" - "github.com/stretchr/testify/require" - "github.com/coder/coder/coderd/authz" + "github.com/stretchr/testify/require" ) // TestExample gives some examples on how to use the authz library. From 0868301401c3118deba7a6d69c24403fd5887b79 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 8 Apr 2022 12:27:22 -0500 Subject: [PATCH 55/75] fixup! correct comments --- coderd/authz/authz.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index 5af34f35f2bce..c11bb94dde7e7 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -15,7 +15,7 @@ func Authorize(subj Subject, obj Resource, action Action) error { // Merge before we send to rego to optimize the json payload. // TODO: Benchmark the rego, it might be ok to just send everything and let - // rego do the merges. The number of roles will be small, so it might not + // rego handle it. The number of roles will be small, so it might not // matter. This code exists just to show how you can merge the roles // into a single one for evaluation if need be. // If done in rego, the roles will not be merged, and just walked over From 512c09ed4fc01ec3b5409d7faf411657758dd8b4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Sun, 10 Apr 2022 13:23:25 -0500 Subject: [PATCH 56/75] Address PR comments - Drop Resource inteface, use concrete struct for now - Rename some roles - Some comments --- coderd/authz/authz.go | 8 ++--- coderd/authz/authz_test.go | 20 ++++++------ coderd/authz/example_test.go | 4 +-- coderd/authz/object.go | 60 +++++++++++++----------------------- coderd/authz/permission.go | 9 ------ coderd/authz/resources.go | 39 ++++++++++------------- coderd/authz/role.go | 17 ++++++++-- 7 files changed, 69 insertions(+), 88 deletions(-) delete mode 100644 coderd/authz/permission.go diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index c11bb94dde7e7..ef70af1aed11c 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -5,7 +5,7 @@ import "golang.org/x/xerrors" var ErrUnauthorized = xerrors.New("unauthorized") // TODO: Implement Authorize. This will be implmented in mainly rego. -func Authorize(subj Subject, obj Resource, action Action) error { +func Authorize(subj Subject, obj Object, action Action) error { // TODO: Expand subject roles into their permissions as appropriate. Apply scopes. var _, _, _ = subj, obj, action roles, err := subj.GetRoles() @@ -28,13 +28,13 @@ func Authorize(subj Subject, obj Resource, action Action) error { merged.Site = append(merged.Site, r.Site...) // Only grab user roles if the resource is owned by a user. // These roles only apply if the subject is said owner. - if obj.OwnerID() != "" && obj.OwnerID() == subj.ID() { + if obj.Owner != "" && obj.Owner == subj.ID() { merged.User = append(merged.User, r.User...) } // Grab org roles if the resource is owned by a given organization. - if obj.OrgOwnerID() != "" { - orgID := obj.OrgOwnerID() + if obj.OrgOwner != "" { + orgID := obj.OrgOwner if v, ok := r.Org[orgID]; ok { merged.Org[orgID] = append(merged.Org[orgID], v...) } diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index aaa393282f3da..eb9cb7b1d44f0 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -31,7 +31,7 @@ func TestAuthorizeDomain(t *testing.T) { {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace, actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, @@ -75,7 +75,7 @@ func TestAuthorizeDomain(t *testing.T) { {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace, actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, @@ -122,7 +122,7 @@ func TestAuthorizeDomain(t *testing.T) { {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace, actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, @@ -169,7 +169,7 @@ func TestAuthorizeDomain(t *testing.T) { {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace, actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: true}, // Other org + me + id {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, @@ -216,7 +216,7 @@ func TestAuthorizeDomain(t *testing.T) { {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace, actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, @@ -286,7 +286,7 @@ func TestAuthorizeDomain(t *testing.T) { {resource: authz.ResourceWorkspace.SetID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace, allow: false}, + {resource: authz.ResourceWorkspace.All(), allow: false}, // Other org + me + id {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), allow: false}, @@ -331,7 +331,7 @@ func TestAuthorizeDomain(t *testing.T) { {resource: authz.ResourceWorkspace.SetID(wrkID)}, - {resource: authz.ResourceWorkspace}, + {resource: authz.ResourceWorkspace.All()}, // Other org + me + id {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID)}, @@ -401,7 +401,7 @@ func TestAuthorizeLevels(t *testing.T) { {resource: authz.ResourceWorkspace.SetID(wrkID)}, - {resource: authz.ResourceWorkspace}, + {resource: authz.ResourceWorkspace.All()}, // Other org + me + id {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID)}, @@ -474,7 +474,7 @@ func TestAuthorizeLevels(t *testing.T) { {resource: authz.ResourceWorkspace.SetID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace, allow: false}, + {resource: authz.ResourceWorkspace.All(), allow: false}, // Other org + me + id {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), allow: false}, @@ -514,7 +514,7 @@ func cases(opt func(c authTestCase) authTestCase, cases []authTestCase) []authTe } type authTestCase struct { - resource authz.Resource + resource authz.Object actions []authz.Action allow bool } diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index e61fa79ff1530..7f436b94b1fef 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -28,7 +28,7 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("ReadAllWorkspaces", func(t *testing.T) { // To read all workspaces on the site - err := authz.Authorize(user, authz.ResourceWorkspace, authz.ActionRead) + err := authz.Authorize(user, authz.ResourceWorkspace.All(), authz.ActionRead) var _ = err // require.Error(t, err, "this user cannot read all workspaces") }) @@ -52,7 +52,7 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("CreateNewSiteUser", func(t *testing.T) { - err := authz.Authorize(user, authz.ResourceUser, authz.ActionCreate) + err := authz.Authorize(user, authz.ResourceUser.All(), authz.ActionCreate) var _ = err // require.Error(t, err, "this user cannot create new users") }) diff --git a/coderd/authz/object.go b/coderd/authz/object.go index 41a353a351c67..6df9b06513ba3 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -1,62 +1,46 @@ package authz -type Resource interface { - ID() string - ResourceType() ResourceType +//type Resource interface { +// ID() string +// ResourceType() ResourceType +// +// OwnerID() string +// OrgOwnerID() string +//} - OwnerID() string - OrgOwnerID() string -} - -var _ Resource = (*zObject)(nil) +//var _ Resource = (*Object)(nil) -// zObject is used to create objects for authz checks when you have none in +// Object 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 +// An example is if you want to list all workspaces, you can create a Object // 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 - orgOwner string +type Object struct { + ID string + Owner string + OrgOwner string - // objectType is "workspace", "project", "devurl", etc - objectType ResourceType + // ObjectType is "workspace", "project", "devurl", etc + ObjectType ResourceType // TODO: SharedUsers? } -func (z zObject) ID() string { - return z.id -} - -func (z zObject) ResourceType() ResourceType { - return z.objectType -} - -func (z zObject) OwnerID() string { - return z.owner -} - -func (z zObject) OrgOwnerID() string { - return z.orgOwner -} - // SetOrg adds an org OwnerID to the resource //nolint:revive -func (z zObject) SetOrg(orgID string) zObject { - z.orgOwner = orgID +func (z Object) SetOrg(orgID string) Object { + z.OrgOwner = orgID return z } // SetOwner adds an OwnerID to the resource //nolint:revive -func (z zObject) SetOwner(id string) zObject { - z.owner = id +func (z Object) SetOwner(id string) Object { + z.Owner = id return z } //nolint:revive -func (z zObject) SetID(id string) zObject { - z.id = id +func (z Object) SetID(id string) Object { + z.ID = id return z } diff --git a/coderd/authz/permission.go b/coderd/authz/permission.go deleted file mode 100644 index bb8ffdb3452b0..0000000000000 --- a/coderd/authz/permission.go +++ /dev/null @@ -1,9 +0,0 @@ -package authz - -type Permission struct { - // Negate makes this a negative permission - Negate bool - ResourceType ResourceType - ResourceID string - Action Action -} diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index cf1b5a94f066a..a92258b608132 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -5,46 +5,41 @@ type ResourceType string const ( ResourceWorkspace ResourceType = "workspace" - ResourceProject ResourceType = "project" + ResourceTemplate ResourceType = "template" ResourceDevURL ResourceType = "devurl" ResourceUser ResourceType = "user" ResourceAuditLogs ResourceType = "audit-logs" ) -func (ResourceType) ID() string { - return "" -} - -func (t ResourceType) ResourceType() ResourceType { - return t +func (z ResourceType) All() Object { + return Object{ + ObjectType: z, + } } -func (ResourceType) OwnerID() string { return "" } -func (ResourceType) OrgOwnerID() string { return "" } - // SetOrg adds an org OwnerID to the resource //nolint:revive -func (r ResourceType) SetOrg(orgID string) zObject { - return zObject{ - orgOwner: orgID, - objectType: r, +func (r ResourceType) SetOrg(orgID string) Object { + return Object{ + OrgOwner: orgID, + ObjectType: r, } } // SetOwner adds an OwnerID to the resource //nolint:revive -func (r ResourceType) SetOwner(id string) zObject { - return zObject{ - owner: id, - objectType: r, +func (r ResourceType) SetOwner(id string) Object { + return Object{ + Owner: id, + ObjectType: r, } } // SetID adds a resource ID to the resource //nolint:revive -func (r ResourceType) SetID(id string) zObject { - return zObject{ - id: id, - objectType: r, +func (r ResourceType) SetID(id string) Object { + return Object{ + ID: id, + ObjectType: r, } } diff --git a/coderd/authz/role.go b/coderd/authz/role.go index 7d26f6234eba3..b582f79d68b37 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -4,6 +4,14 @@ import "fmt" const Wildcard = "*" +type Permission struct { + // Negate makes this a negative permission + Negate bool + ResourceType ResourceType + ResourceID string + Action Action +} + // Role is a set of permissions at multiple levels: // - Site level permissions apply EVERYWHERE // - Org level permissions apply to EVERYTHING in a given ORG @@ -13,6 +21,9 @@ const Wildcard = "*" type Role struct { Name string Site []Permission + // Org is a map of orgid to permissions. We represent orgid as a string. + // TODO: Maybe switch to uuid, but tokens might need to support a "wildcard" org + // which could be a special uuid (like all 0s?) Org map[string][]Permission User []Permission } @@ -22,7 +33,7 @@ type Role struct { var ( // RoleSiteAdmin is a role that allows everything everywhere. RoleSiteAdmin = Role{ - Name: "site-admin", + Name: "admin", Site: permissions(map[ResourceType][]Action{ Wildcard: {Wildcard}, }), @@ -30,7 +41,7 @@ var ( // RoleSiteMember is a role that allows access to user-level resources. RoleSiteMember = Role{ - Name: "site-member", + Name: "member", User: permissions(map[ResourceType][]Action{ Wildcard: {Wildcard}, }), @@ -38,7 +49,7 @@ var ( // RoleSiteAuditor is an example on how to give more precise permissions RoleSiteAuditor = Role{ - Name: "site-auditor", + Name: "auditor", Site: permissions(map[ResourceType][]Action{ ResourceAuditLogs: {ActionRead}, // Should be able to read user details to associate with logs. From d9f761d5fe1ac5ad4f4d15117fee06734ad497cd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 10:07:12 -0500 Subject: [PATCH 57/75] Drop unused resources & roles --- coderd/authz/authz_test.go | 13 ++++++++++++- coderd/authz/resources.go | 2 -- coderd/authz/role.go | 17 ++--------------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index eb9cb7b1d44f0..6e9f7de083bd8 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -60,7 +60,18 @@ func TestAuthorizeDomain(t *testing.T) { user = authz.SubjectTODO{ UserID: "me", - Roles: []authz.Role{authz.RoleDenyAll}, + Roles: []authz.Role{{ + Name: "deny-all", + // List out deny permissions explicitly + Site: []authz.Permission{ + { + Negate: true, + ResourceType: authz.Wildcard, + ResourceID: authz.Wildcard, + Action: authz.Wildcard, + }, + }, + }}, } testAuthorize(t, "DeletedMember", user, []authTestCase{ diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index a92258b608132..3b239bbcfdce1 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -6,9 +6,7 @@ type ResourceType string const ( ResourceWorkspace ResourceType = "workspace" ResourceTemplate ResourceType = "template" - ResourceDevURL ResourceType = "devurl" ResourceUser ResourceType = "user" - ResourceAuditLogs ResourceType = "audit-logs" ) func (z ResourceType) All() Object { diff --git a/coderd/authz/role.go b/coderd/authz/role.go index b582f79d68b37..d9976b42d204f 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -51,26 +51,13 @@ var ( RoleSiteAuditor = Role{ Name: "auditor", Site: permissions(map[ResourceType][]Action{ - ResourceAuditLogs: {ActionRead}, + // TODO: @emyrk when audit logs are added, add back a read perm + //ResourceAuditLogs: {ActionRead}, // Should be able to read user details to associate with logs. // Without this the user-id in logs is not very helpful ResourceUser: {ActionRead}, }), } - - // RoleDenyAll is a role that denies everything everywhere. - RoleDenyAll = Role{ - Name: "deny-all", - // List out deny permissions explicitly - Site: []Permission{ - { - Negate: true, - ResourceType: Wildcard, - ResourceID: Wildcard, - Action: Wildcard, - }, - }, - } ) func RoleOrgDenyAll(orgID string) Role { From 81ca08a9f5ae18f0a418793f653c9a89fdbdd5fb Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 10:08:41 -0500 Subject: [PATCH 58/75] Rename chaining functions --- coderd/authz/authz_test.go | 396 +++++++++++++++++------------------ coderd/authz/example_test.go | 6 +- coderd/authz/object.go | 10 +- coderd/authz/resources.go | 12 +- 4 files changed, 212 insertions(+), 212 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 6e9f7de083bd8..dddd3dab21a28 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -21,41 +21,41 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "Member", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, }) user = authz.SubjectTODO{ @@ -76,41 +76,41 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "DeletedMember", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, }) user = authz.SubjectTODO{ @@ -123,41 +123,41 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "OrgAdmin", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, }) user = authz.SubjectTODO{ @@ -170,41 +170,41 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "SiteAdmin", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: true}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetID("not-id"), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: true}, }) // In practice this is a token scope on a regular subject @@ -217,41 +217,41 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "WorkspaceAgentToken", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.SetID("not-id"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, }) // In practice this is a token scope on a regular subject @@ -287,41 +287,41 @@ func TestAuthorizeDomain(t *testing.T) { }, []authTestCase{ // Read // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()), allow: true}, - {resource: authz.ResourceWorkspace.SetID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.WithID(wrkID), allow: false}, {resource: authz.ResourceWorkspace.All(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, - {resource: authz.ResourceWorkspace.SetID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.WithID("not-id"), allow: false}, }), // Pass non-read actions @@ -332,41 +332,41 @@ func TestAuthorizeDomain(t *testing.T) { }, []authTestCase{ // Read // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID())}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID())}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg)}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID())}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID())}, - {resource: authz.ResourceWorkspace.SetID(wrkID)}, + {resource: authz.ResourceWorkspace.WithID(wrkID)}, {resource: authz.ResourceWorkspace.All()}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID())}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOrg("other")}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID())}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg("other")}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me")}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOwner("not-me")}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID)}, + {resource: authz.ResourceWorkspace.WithOwner("not-me")}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id")}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me")}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id")}, - {resource: authz.ResourceWorkspace.SetOrg("other")}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me")}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id")}, + {resource: authz.ResourceWorkspace.InOrg("other")}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id")}, - {resource: authz.ResourceWorkspace.SetOwner("not-me")}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id")}, + {resource: authz.ResourceWorkspace.WithOwner("not-me")}, - {resource: authz.ResourceWorkspace.SetID("not-id")}, + {resource: authz.ResourceWorkspace.WithID("not-id")}, })) } @@ -402,41 +402,41 @@ func TestAuthorizeLevels(t *testing.T) { return c }, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID())}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID())}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg)}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID())}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID())}, - {resource: authz.ResourceWorkspace.SetID(wrkID)}, + {resource: authz.ResourceWorkspace.WithID(wrkID)}, {resource: authz.ResourceWorkspace.All()}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID())}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOrg("other")}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID())}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg("other")}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me")}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID)}, - {resource: authz.ResourceWorkspace.SetOwner("not-me")}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID)}, + {resource: authz.ResourceWorkspace.WithOwner("not-me")}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id")}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me")}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id")}, - {resource: authz.ResourceWorkspace.SetOrg("other")}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me")}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id")}, + {resource: authz.ResourceWorkspace.InOrg("other")}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id")}, - {resource: authz.ResourceWorkspace.SetOwner("not-me")}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id")}, + {resource: authz.ResourceWorkspace.WithOwner("not-me")}, - {resource: authz.ResourceWorkspace.SetID("not-id")}, + {resource: authz.ResourceWorkspace.WithID("not-id")}, })) user = authz.SubjectTODO{ @@ -475,41 +475,41 @@ func TestAuthorizeLevels(t *testing.T) { return c }, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()).SetID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner(user.ID()), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()).SetID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner(user.ID()), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner(user.ID()), allow: false}, - {resource: authz.ResourceWorkspace.SetID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.WithID(wrkID), allow: false}, {resource: authz.ResourceWorkspace.All(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()).SetID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner(user.ID()), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, // Other org + other user + id - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me").SetID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.SetOrg(defOrg).SetOwner("not-me"), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: true}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, // Other org + other use + other id - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me").SetID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetOwner("not-me"), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other").SetID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.SetOrg("other"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me").SetID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.SetOwner("not-me"), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, - {resource: authz.ResourceWorkspace.SetID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.WithID("not-id"), allow: false}, })) } diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index 7f436b94b1fef..2e30132d52d9d 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -36,17 +36,17 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("ReadOrgWorkspaces", func(t *testing.T) { // To read all workspaces on the org 'default' - err := authz.Authorize(user, authz.ResourceWorkspace.SetOrg("default"), authz.ActionRead) + err := authz.Authorize(user, authz.ResourceWorkspace.InOrg("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.SetOrg("default").SetOwner(user.UserID), authz.ActionRead) + err := authz.Authorize(user, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID), authz.ActionRead) require.NoError(t, err, "this user can their workspace") - err = authz.Authorize(user, authz.ResourceWorkspace.SetOrg("default").SetOwner(user.UserID).SetID("1234"), authz.ActionRead) + err = authz.Authorize(user, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234"), authz.ActionRead) require.NoError(t, err, "this user can read workspace '1234'") }) diff --git a/coderd/authz/object.go b/coderd/authz/object.go index 6df9b06513ba3..3250ab956d0ba 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -25,22 +25,22 @@ type Object struct { // TODO: SharedUsers? } -// SetOrg adds an org OwnerID to the resource +// InOrg adds an org OwnerID to the resource //nolint:revive -func (z Object) SetOrg(orgID string) Object { +func (z Object) InOrg(orgID string) Object { z.OrgOwner = orgID return z } -// SetOwner adds an OwnerID to the resource +// WithOwner adds an OwnerID to the resource //nolint:revive -func (z Object) SetOwner(id string) Object { +func (z Object) WithOwner(id string) Object { z.Owner = id return z } //nolint:revive -func (z Object) SetID(id string) Object { +func (z Object) WithID(id string) Object { z.ID = id return z } diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index 3b239bbcfdce1..30a17972a0d3f 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -15,27 +15,27 @@ func (z ResourceType) All() Object { } } -// SetOrg adds an org OwnerID to the resource +// InOrg adds an org OwnerID to the resource //nolint:revive -func (r ResourceType) SetOrg(orgID string) Object { +func (r ResourceType) InOrg(orgID string) Object { return Object{ OrgOwner: orgID, ObjectType: r, } } -// SetOwner adds an OwnerID to the resource +// WithOwner adds an OwnerID to the resource //nolint:revive -func (r ResourceType) SetOwner(id string) Object { +func (r ResourceType) WithOwner(id string) Object { return Object{ Owner: id, ObjectType: r, } } -// SetID adds a resource ID to the resource +// WithID adds a resource ID to the resource //nolint:revive -func (r ResourceType) SetID(id string) Object { +func (r ResourceType) WithID(id string) Object { return Object{ ID: id, ObjectType: r, From fced411a51a4831d252c47c93801680f4332564c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 12:45:45 -0500 Subject: [PATCH 59/75] Rename OrgOwner -> OrgID --- coderd/authz/README.md | 2 +- coderd/authz/authz.go | 4 ++-- coderd/authz/authz_test.go | 10 +++++----- coderd/authz/example_test.go | 2 +- coderd/authz/object.go | 11 ++++++----- coderd/authz/resources.go | 2 +- coderd/authz/role.go | 12 ++++++------ 7 files changed, 22 insertions(+), 21 deletions(-) diff --git a/coderd/authz/README.md b/coderd/authz/README.md index fe8c162091ebe..69ba5e9f925aa 100644 --- a/coderd/authz/README.md +++ b/coderd/authz/README.md @@ -4,7 +4,7 @@ Package `authz` implements AuthoriZation for Coder. ## Overview -Authorization defines what **permission** an **subject** has to perform **actions** to **objects**: +Authorization defines what **permission** a **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. diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index ef70af1aed11c..26b6aee278d6d 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -33,8 +33,8 @@ func Authorize(subj Subject, obj Object, action Action) error { } // Grab org roles if the resource is owned by a given organization. - if obj.OrgOwner != "" { - orgID := obj.OrgOwner + if obj.OrgID != "" { + orgID := obj.OrgID if v, ok := r.Org[orgID]; ok { merged.Org[orgID] = append(merged.Org[orgID], v...) } diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index dddd3dab21a28..d6370f937235d 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -16,7 +16,7 @@ func TestAuthorizeDomain(t *testing.T) { user := authz.SubjectTODO{ UserID: "me", - Roles: []authz.Role{authz.RoleSiteMember, authz.RoleOrgMember(defOrg)}, + Roles: []authz.Role{authz.RoleMember, authz.RoleOrgMember(defOrg)}, } testAuthorize(t, "Member", user, []authTestCase{ @@ -117,7 +117,7 @@ func TestAuthorizeDomain(t *testing.T) { UserID: "me", Roles: []authz.Role{ authz.RoleOrgAdmin(defOrg), - authz.RoleSiteMember, + authz.RoleMember, }, } @@ -163,8 +163,8 @@ func TestAuthorizeDomain(t *testing.T) { user = authz.SubjectTODO{ UserID: "me", Roles: []authz.Role{ - authz.RoleSiteAdmin, - authz.RoleSiteMember, + authz.RoleAdmin, + authz.RoleMember, }, } @@ -378,7 +378,7 @@ func TestAuthorizeLevels(t *testing.T) { user := authz.SubjectTODO{ UserID: "me", Roles: []authz.Role{ - authz.RoleSiteAdmin, + authz.RoleAdmin, authz.RoleOrgDenyAll(defOrg), { Name: "user-deny-all", diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index 2e30132d52d9d..13b71996b223e 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -19,7 +19,7 @@ func TestExample(t *testing.T) { UserID: "alice", Roles: []authz.Role{ authz.RoleOrgAdmin("default"), - authz.RoleSiteMember, + authz.RoleMember, }, } diff --git a/coderd/authz/object.go b/coderd/authz/object.go index 3250ab956d0ba..25ab2af7025d2 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -16,19 +16,20 @@ package authz // 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 Object struct { - ID string - Owner string - OrgOwner string + ID string `json:"id"` + Owner string `json:"owner"` + // OrgID specifies which org the object is a part of. + OrgID string `json:"org_owner"` // ObjectType is "workspace", "project", "devurl", etc - ObjectType ResourceType + ObjectType ResourceType `json:"object_type"` // TODO: SharedUsers? } // InOrg adds an org OwnerID to the resource //nolint:revive func (z Object) InOrg(orgID string) Object { - z.OrgOwner = orgID + z.OrgID = orgID return z } diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index 30a17972a0d3f..77ef3bd6f4731 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -19,7 +19,7 @@ func (z ResourceType) All() Object { //nolint:revive func (r ResourceType) InOrg(orgID string) Object { return Object{ - OrgOwner: orgID, + OrgID: orgID, ObjectType: r, } } diff --git a/coderd/authz/role.go b/coderd/authz/role.go index d9976b42d204f..62222c40d8a88 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -31,24 +31,24 @@ type Role struct { // Roles are stored as structs, so they can be serialized and stored. Until we store them elsewhere, // const's will do just fine. var ( - // RoleSiteAdmin is a role that allows everything everywhere. - RoleSiteAdmin = Role{ + // RoleAdmin is a role that allows everything everywhere. + RoleAdmin = Role{ Name: "admin", Site: permissions(map[ResourceType][]Action{ Wildcard: {Wildcard}, }), } - // RoleSiteMember is a role that allows access to user-level resources. - RoleSiteMember = Role{ + // RoleMember is a role that allows access to user-level resources. + RoleMember = Role{ Name: "member", User: permissions(map[ResourceType][]Action{ Wildcard: {Wildcard}, }), } - // RoleSiteAuditor is an example on how to give more precise permissions - RoleSiteAuditor = Role{ + // RoleAuditor is an example on how to give more precise permissions + RoleAuditor = Role{ Name: "auditor", Site: permissions(map[ResourceType][]Action{ // TODO: @emyrk when audit logs are added, add back a read perm From f620ebfad19663426c3ef035d37ddfc7230d8959 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 19:10:31 -0500 Subject: [PATCH 60/75] feat: Add rego policy implementation --- coderd/authz/authz.go | 45 ++---- coderd/authz/authz_test.go | 274 ++++++++++++++++++++++------------- coderd/authz/error.go | 43 ++++++ coderd/authz/example_test.go | 19 +-- coderd/authz/object.go | 27 ++-- coderd/authz/policy.rego | 123 ++++++++++++++++ coderd/authz/rego.go | 68 +++++++++ coderd/authz/resources.go | 45 ++---- coderd/authz/role.go | 40 ++--- coderd/authz/subject.go | 27 ---- 10 files changed, 466 insertions(+), 245 deletions(-) create mode 100644 coderd/authz/error.go create mode 100644 coderd/authz/policy.rego create mode 100644 coderd/authz/rego.go delete mode 100644 coderd/authz/subject.go diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index 26b6aee278d6d..f15877cb56af4 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -1,46 +1,19 @@ package authz -import "golang.org/x/xerrors" +import ( + "context" + "golang.org/x/xerrors" +) var ErrUnauthorized = xerrors.New("unauthorized") // TODO: Implement Authorize. This will be implmented in mainly rego. -func Authorize(subj Subject, obj Object, action Action) error { - // TODO: Expand subject roles into their permissions as appropriate. Apply scopes. - var _, _, _ = subj, obj, action - roles, err := subj.GetRoles() +func Authorize(ctx context.Context, subjID string, roles []Role, obj Object, action Action) error { + // TODO: Cache authorizer + authorizer, err := newAuthorizer() if err != nil { - return ErrUnauthorized + return ForbiddenWithInternal(xerrors.Errorf("new authorizer: %w", err), nil) } - // Merge before we send to rego to optimize the json payload. - // TODO: Benchmark the rego, it might be ok to just send everything and let - // rego handle it. The number of roles will be small, so it might not - // matter. This code exists just to show how you can merge the roles - // into a single one for evaluation if need be. - // If done in rego, the roles will not be merged, and just walked over - // 1 by 1. - var merged Role - for _, r := range roles { - // Site, Org, and User permissions exist on every role. Pull out only the permissions that - // are relevant to the object. - - merged.Site = append(merged.Site, r.Site...) - // Only grab user roles if the resource is owned by a user. - // These roles only apply if the subject is said owner. - if obj.Owner != "" && obj.Owner == subj.ID() { - merged.User = append(merged.User, r.User...) - } - - // Grab org roles if the resource is owned by a given organization. - if obj.OrgID != "" { - orgID := obj.OrgID - if v, ok := r.Org[orgID]; ok { - merged.Org[orgID] = append(merged.Org[orgID], v...) - } - } - } - - // TODO: Send to rego policy evaluation. - return nil + return authorizer.Authorize(ctx, subjID, roles, obj, action) } diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index d6370f937235d..d925f2b3cced4 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -1,41 +1,50 @@ package authz_test import ( + "context" + "encoding/json" + "golang.org/x/xerrors" "testing" "github.com/coder/coder/coderd/authz" "github.com/stretchr/testify/require" ) +// subject is required because rego needs +type subject struct { + UserID string `json:"id"` + Roles []authz.Role `json:"roles"` +} + // TestAuthorizeDomain test the very basic roles that are commonly used. func TestAuthorizeDomain(t *testing.T) { - t.Skip("TODO: unskip when rego is done") + //t.Skip("TODO: unskip when rego is done") t.Parallel() defOrg := "default" wrkID := "1234" - user := authz.SubjectTODO{ + user := subject{ UserID: "me", Roles: []authz.Role{authz.RoleMember, authz.RoleOrgMember(defOrg)}, } testAuthorize(t, "Member", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, @@ -58,7 +67,7 @@ func TestAuthorizeDomain(t *testing.T) { {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, }) - user = authz.SubjectTODO{ + user = subject{ UserID: "me", Roles: []authz.Role{{ Name: "deny-all", @@ -66,9 +75,9 @@ func TestAuthorizeDomain(t *testing.T) { Site: []authz.Permission{ { Negate: true, - ResourceType: authz.Wildcard, - ResourceID: authz.Wildcard, - Action: authz.Wildcard, + ResourceType: authz.WildcardSymbol, + ResourceID: authz.WildcardSymbol, + Action: authz.WildcardSymbol, }, }, }}, @@ -76,21 +85,21 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "DeletedMember", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, @@ -113,7 +122,7 @@ func TestAuthorizeDomain(t *testing.T) { {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, }) - user = authz.SubjectTODO{ + user = subject{ UserID: "me", Roles: []authz.Role{ authz.RoleOrgAdmin(defOrg), @@ -123,21 +132,21 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "OrgAdmin", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, @@ -160,7 +169,7 @@ func TestAuthorizeDomain(t *testing.T) { {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, }) - user = authz.SubjectTODO{ + user = subject{ UserID: "me", Roles: []authz.Role{ authz.RoleAdmin, @@ -170,21 +179,21 @@ func TestAuthorizeDomain(t *testing.T) { testAuthorize(t, "SiteAdmin", user, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: true}, // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: true}, {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true}, @@ -208,57 +217,107 @@ func TestAuthorizeDomain(t *testing.T) { }) // In practice this is a token scope on a regular subject - user = authz.SubjectTODO{ + user = subject{ UserID: "me", Roles: []authz.Role{ authz.RoleWorkspaceAgent(wrkID), }, } - testAuthorize(t, "WorkspaceAgentToken", user, []authTestCase{ - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false}, + testAuthorize(t, "WorkspaceAgentToken", user, + // Read Actions + cases(func(c authTestCase) authTestCase { + c.actions = []authz.Action{authz.ActionRead} + return c + }, []authTestCase{ + // Org + me + id + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID), allow: false}, - {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: true}, + {resource: authz.ResourceWorkspace.WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.All(), allow: false}, - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, + // Other org + me + id + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: false}, + // Other org + other user + id + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, + // Other org + other use + other id + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, - {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, - }) + {resource: authz.ResourceWorkspace.WithID("not-id"), allow: false}, + }), + // Not read actions + cases(func(c authTestCase) authTestCase { + c.actions = []authz.Action{authz.ActionCreate, authz.ActionUpdate, authz.ActionDelete} + c.allow = false + return c + }, []authTestCase{ + // Org + me + id + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg)}, + + {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID)}, + + {resource: authz.ResourceWorkspace.WithID(wrkID)}, + + {resource: authz.ResourceWorkspace.All()}, + + // Other org + me + id + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg("other")}, + + // Other org + other user + id + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")}, + + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID)}, + {resource: authz.ResourceWorkspace.WithOwner("not-me")}, + + // Other org + other use + other id + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me")}, + {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id")}, + {resource: authz.ResourceWorkspace.InOrg("other")}, + + {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id")}, + {resource: authz.ResourceWorkspace.WithOwner("not-me")}, + + {resource: authz.ResourceWorkspace.WithID("not-id")}, + }), + ) // In practice this is a token scope on a regular subject - user = authz.SubjectTODO{ + user = subject{ UserID: "me", Roles: []authz.Role{ { + Name: "ReadOnlyOrgAndUser", Site: []authz.Permission{}, Org: map[string][]authz.Permission{ defOrg: {{ @@ -287,21 +346,21 @@ func TestAuthorizeDomain(t *testing.T) { }, []authTestCase{ // Read // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: true}, {resource: authz.ResourceWorkspace.InOrg(defOrg), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID), allow: true}, {resource: authz.ResourceWorkspace.WithID(wrkID), allow: false}, {resource: authz.ResourceWorkspace.All(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false}, {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false}, {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, @@ -332,21 +391,21 @@ func TestAuthorizeDomain(t *testing.T) { }, []authTestCase{ // Read // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID())}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID)}, {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, {resource: authz.ResourceWorkspace.InOrg(defOrg)}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID())}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID)}, {resource: authz.ResourceWorkspace.WithID(wrkID)}, {resource: authz.ResourceWorkspace.All()}, // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID())}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)}, {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID)}, {resource: authz.ResourceWorkspace.InOrg("other")}, @@ -375,7 +434,7 @@ func TestAuthorizeLevels(t *testing.T) { defOrg := "default" wrkID := "1234" - user := authz.SubjectTODO{ + user := subject{ UserID: "me", Roles: []authz.Role{ authz.RoleAdmin, @@ -386,9 +445,9 @@ func TestAuthorizeLevels(t *testing.T) { Site: []authz.Permission{ { Negate: true, - ResourceType: authz.Wildcard, - ResourceID: authz.Wildcard, - Action: authz.Wildcard, + ResourceType: authz.WildcardSymbol, + ResourceID: authz.WildcardSymbol, + Action: authz.WildcardSymbol, }, }, }, @@ -402,21 +461,21 @@ func TestAuthorizeLevels(t *testing.T) { return c }, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID())}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID)}, {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, {resource: authz.ResourceWorkspace.InOrg(defOrg)}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID())}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID)}, {resource: authz.ResourceWorkspace.WithID(wrkID)}, {resource: authz.ResourceWorkspace.All()}, // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID())}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)}, {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID)}, {resource: authz.ResourceWorkspace.InOrg("other")}, @@ -439,7 +498,7 @@ func TestAuthorizeLevels(t *testing.T) { {resource: authz.ResourceWorkspace.WithID("not-id")}, })) - user = authz.SubjectTODO{ + user = subject{ UserID: "me", Roles: []authz.Role{ { @@ -448,8 +507,8 @@ func TestAuthorizeLevels(t *testing.T) { { Negate: true, ResourceType: "random", - ResourceID: authz.Wildcard, - Action: authz.Wildcard, + ResourceID: authz.WildcardSymbol, + Action: authz.WildcardSymbol, }, }, }, @@ -460,9 +519,9 @@ func TestAuthorizeLevels(t *testing.T) { Site: []authz.Permission{ { Negate: true, - ResourceType: authz.Wildcard, - ResourceID: authz.Wildcard, - Action: authz.Wildcard, + ResourceType: authz.WildcardSymbol, + ResourceID: authz.WildcardSymbol, + Action: authz.WildcardSymbol, }, }, }, @@ -475,21 +534,21 @@ func TestAuthorizeLevels(t *testing.T) { return c }, []authTestCase{ // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID()), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), allow: true}, {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: true}, {resource: authz.ResourceWorkspace.InOrg(defOrg), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()).WithID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner(user.ID()), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.WithOwner(user.UserID), allow: false}, {resource: authz.ResourceWorkspace.WithID(wrkID), allow: false}, {resource: authz.ResourceWorkspace.All(), allow: false}, // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()).WithID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.ID()), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: false}, + {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false}, {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false}, {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, @@ -530,16 +589,31 @@ type authTestCase struct { allow bool } -func testAuthorize(t *testing.T, name string, subject authz.Subject, sets ...[]authTestCase) { +func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTestCase) { for _, cases := range sets { for _, c := range cases { t.Run(name, func(t *testing.T) { for _, a := range c.actions { - err := authz.Authorize(subject, c.resource, a) + err := authz.Authorize(context.Background(), subject.UserID, subject.Roles, c.resource, a) if c.allow { + if err != nil { + var uerr *authz.Unauthorized + xerrors.As(err, &uerr) + d, _ := json.Marshal(uerr.Input()) + t.Log(string(d)) + } require.NoError(t, err, "expected no error for testcase action %s", a) continue } + + if err == nil { + d, _ := json.Marshal(map[string]interface{}{ + "subject": subject, + "object": c.resource, + "action": a, + }) + t.Log(string(d)) + } require.Error(t, err, "expected unauthorized") } }) diff --git a/coderd/authz/error.go b/coderd/authz/error.go new file mode 100644 index 0000000000000..454b88ebdf1be --- /dev/null +++ b/coderd/authz/error.go @@ -0,0 +1,43 @@ +package authz + +const ( + // UnauthorizedErrorMessage is the error message that should be returned to + // clients when an action is forbidden. It is intentionally vague to prevent + // disclosing information that a client should not have access to. + UnauthorizedErrorMessage = "unauthorized" +) + +// Unauthorized is the error type for authorization errors +type Unauthorized struct { + // internal is the internal error that should never be shown to the client. + // It is only for debugging purposes. + internal error + input map[string]interface{} +} + +// ForbiddenWithInternal creates a new error that will return a simple +// "forbidden" to the client, logging internally the more detailed message +// provided. +func ForbiddenWithInternal(internal error, input map[string]interface{}) *Unauthorized { + if input == nil { + input = map[string]interface{}{} + } + return &Unauthorized{ + internal: internal, + input: input, + } +} + +// Error implements the error interface. +func (e *Unauthorized) Error() string { + return UnauthorizedErrorMessage +} + +// Internal allows the internal error message to be logged. +func (e *Unauthorized) Internal() error { + return e.internal +} + +func (e *Unauthorized) Input() map[string]interface{} { + return e.input +} diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index 13b71996b223e..1e4cc043c0d97 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -1,6 +1,7 @@ package authz_test import ( + "context" "testing" "github.com/coder/coder/coderd/authz" @@ -12,10 +13,11 @@ import ( func TestExample(t *testing.T) { t.Skip("TODO: unskip when rego is done") t.Parallel() + ctx := context.Background() // 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{ + user := subject{ UserID: "alice", Roles: []authz.Role{ authz.RoleOrgAdmin("default"), @@ -28,7 +30,7 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("ReadAllWorkspaces", func(t *testing.T) { // To read all workspaces on the site - err := authz.Authorize(user, authz.ResourceWorkspace.All(), authz.ActionRead) + err := authz.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.All(), authz.ActionRead) var _ = err // require.Error(t, err, "this user cannot read all workspaces") }) @@ -36,24 +38,17 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("ReadOrgWorkspaces", func(t *testing.T) { // To read all workspaces on the org 'default' - err := authz.Authorize(user, authz.ResourceWorkspace.InOrg("default"), authz.ActionRead) + err := authz.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("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.InOrg("default").WithOwner(user.UserID), authz.ActionRead) + err := authz.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID), authz.ActionRead) require.NoError(t, err, "this user can their workspace") - err = authz.Authorize(user, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234"), authz.ActionRead) + err = authz.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234"), authz.ActionRead) 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.All(), authz.ActionCreate) - var _ = err - // require.Error(t, err, "this user cannot create new users") - }) } diff --git a/coderd/authz/object.go b/coderd/authz/object.go index 25ab2af7025d2..dd69b81dc7897 100644 --- a/coderd/authz/object.go +++ b/coderd/authz/object.go @@ -1,31 +1,28 @@ package authz -//type Resource interface { -// ID() string -// ResourceType() ResourceType -// -// OwnerID() string -// OrgOwnerID() string -//} - -//var _ Resource = (*Object)(nil) - // Object 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 Object // 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 Object struct { - ID string `json:"id"` - Owner string `json:"owner"` + ResourceID string `json:"id"` + Owner string `json:"owner"` // OrgID specifies which org the object is a part of. OrgID string `json:"org_owner"` - // ObjectType is "workspace", "project", "devurl", etc - ObjectType ResourceType `json:"object_type"` + // Type is "workspace", "project", "devurl", etc + Type string `json:"type"` // TODO: SharedUsers? } +func (z Object) All() Object { + z.OrgID = "" + z.Owner = "" + z.ResourceID = "" + return z +} + // InOrg adds an org OwnerID to the resource //nolint:revive func (z Object) InOrg(orgID string) Object { @@ -42,6 +39,6 @@ func (z Object) WithOwner(id string) Object { //nolint:revive func (z Object) WithID(id string) Object { - z.ID = id + z.ResourceID = id return z } diff --git a/coderd/authz/policy.rego b/coderd/authz/policy.rego new file mode 100644 index 0000000000000..cbebe3ff52cfb --- /dev/null +++ b/coderd/authz/policy.rego @@ -0,0 +1,123 @@ +package authz +import future.keywords.in +import future.keywords.every + +# https://play.openpolicyagent.org/p/Jlzq5gIjkd +# grant_allow: https://play.openpolicyagent.org/p/9wpE9x6Tg2 + +# bool_flip lets you assign a value to an inverted bool. +# You cannot do 'x := !false', but you can do 'x := bool_flip(false)' +bool_flip(b) = flipped { + b + flipped = false +} + +bool_flip(b) = flipped { + not b + flipped = true +} + +# perms_grant returns a set of boolean values (true, false). +# It will only return `bool_flip(perm.negate)` for permissions that affect a given +# resource_type, resource_id, and action. +# The empty set is returned if no relevant permissions are found. +perms_grant(permissions) = grants { + # If there are no permissions, this value is the empty set {}. + grants := { x | + # All permissions ... + perm := permissions[_] + # Such that the permission action, type, and resource_id matches + perm.action in [input.action, "*"] + perm.resource_type in [input.object.type, "*"] + perm.resource_id in [input.object.id, "*"] + x := bool_flip(perm.negate) + } +} + +# Site & User are both very simple. We default both to the empty set '{}'. If no permissions are present, then the +# result is the default value. +default site = {} +site = grant { + # Boolean set for all site wide permissions + grant = { v | + v = perms_grant(input.subject.roles[_].site)[_] + } +} + +default user = {} +user = grant { + # Only apply user permissions if the user owns the resource + input.object.owner != "" + input.object.owner == input.subject.id + grant = { v | + v = perms_grant(input.subject.roles[_].user)[_] + } +} + +# Organizations are more complex. If the user has no roles that specifically indicate the org_id of the object, +# then we want to block the action. This is because that means the user is not a member of the org. + +# org_member returns the set of permissions associated with a user if the user is a member of the +# organization +org_member = grant { + input.object.org_owner != "" + grant = { v | + v = perms_grant(input.subject.roles[_].org[input.object.org_owner])[_] + } +} + +# If a user is not part of an organization, 'org_non_member' is set to true +org_non_member { + input.object.org_owner != "" + # Identify if the user is in the org + roles := input.subject.roles + every role in roles { + not role.org[input.object.org_owner] + } +} + +# org is two rules that equate to the following +# if !org_non_member { return org_member } +# else {false} +# +# It is important both rules cannot be true, as the `org` rules cannot produce multiple outputs. +default org = [] +org = set { + not org_non_member + set = org_member +} + +org = set { + org_non_member + set = {false} +} + +# The allow block is quite simple. Any set with `false` cascades down in levels. +# Authorization looks for any `allow` statement that is true. Multiple can be true! + +# site allow +allow { + # No site wide deny + not false in site + # And all permissions are positive + site[_] +} + +# org allow +allow { + # No site or org deny + not false in site + not false in org + # And all permissions are positive + org[_] +} + +# user allow +allow { + # No site, org, or user deny + not false in site + not false in org + not false in user + # And all permissions are positive + user[_] +} \ No newline at end of file diff --git a/coderd/authz/rego.go b/coderd/authz/rego.go new file mode 100644 index 0000000000000..c6e4ccbc3892c --- /dev/null +++ b/coderd/authz/rego.go @@ -0,0 +1,68 @@ +package authz + +import ( + "context" + _ "embed" + + "golang.org/x/xerrors" + + "github.com/open-policy-agent/opa/rego" +) + +// regoAuthorizer will use a prepared rego query for performing authorize() +type regoAuthorizer struct { + query rego.PreparedEvalQuery +} + +// Load the policy from policy.rego in this directory. +//go:embed policy.rego +var policy string + +func newAuthorizer() (*regoAuthorizer, error) { + ctx := context.Background() + query, err := rego.New( + // allowed is the `allow` field from the prepared query. This is the field to check if authorization is + // granted. + rego.Query("allowed = data.authz.allow"), + rego.Module("policy.rego", policy), + ).PrepareForEval(ctx) + + if err != nil { + return nil, xerrors.Errorf("prepare query: %w", err) + } + return ®oAuthorizer{query: query}, nil +} + +type authSubject struct { + ID string `json:"id"` + Roles []Role `json:"roles"` + + SitePermissions []Permission `json:"site_permissions"` + OrgPermissions []Permission `json:"org_permissions"` + UserPermissions []Permission `json:"user_permissions"` +} + +func (a regoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, object Object, action Action) error { + input := map[string]interface{}{ + "subject": authSubject{ + ID: subjectID, + Roles: roles, + }, + "object": object, + "action": action, + } + + results, err := a.query.Eval(ctx, rego.EvalInput(input)) + if err != nil { + return ForbiddenWithInternal(xerrors.Errorf("eval rego: %w, err"), input) + } + + if len(results) != 1 { + return ForbiddenWithInternal(xerrors.Errorf("expect only 1 result, got %d", len(results)), input) + } + + if results[0].Bindings["allowed"] != true { + return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input) + } + return nil +} diff --git a/coderd/authz/resources.go b/coderd/authz/resources.go index 77ef3bd6f4731..1abeb6608957a 100644 --- a/coderd/authz/resources.go +++ b/coderd/authz/resources.go @@ -1,43 +1,18 @@ package authz -// ResourceType defines the list of available resources for authz. -type ResourceType string +const WildcardSymbol = "*" -const ( - ResourceWorkspace ResourceType = "workspace" - ResourceTemplate ResourceType = "template" - ResourceUser ResourceType = "user" -) - -func (z ResourceType) All() Object { - return Object{ - ObjectType: z, +var ( + ResourceWorkspace = Object{ + Type: "workspace", } -} -// InOrg adds an org OwnerID to the resource -//nolint:revive -func (r ResourceType) InOrg(orgID string) Object { - return Object{ - OrgID: orgID, - ObjectType: r, + ResourceTemplate = Object{ + Type: "template", } -} -// WithOwner adds an OwnerID to the resource -//nolint:revive -func (r ResourceType) WithOwner(id string) Object { - return Object{ - Owner: id, - ObjectType: r, + // ResourceWildcard represents all resource types + ResourceWildcard = Object{ + Type: WildcardSymbol, } -} - -// WithID adds a resource ID to the resource -//nolint:revive -func (r ResourceType) WithID(id string) Object { - return Object{ - ID: id, - ObjectType: r, - } -} +) diff --git a/coderd/authz/role.go b/coderd/authz/role.go index 62222c40d8a88..baabe64f6d364 100644 --- a/coderd/authz/role.go +++ b/coderd/authz/role.go @@ -2,14 +2,12 @@ package authz import "fmt" -const Wildcard = "*" - type Permission struct { // Negate makes this a negative permission - Negate bool - ResourceType ResourceType - ResourceID string - Action Action + Negate bool `json:"negate"` + ResourceType string `json:"resource_type"` + ResourceID string `json:"resource_id"` + Action Action `json:"action"` } // Role is a set of permissions at multiple levels: @@ -19,13 +17,13 @@ type Permission struct { // In most cases, you will just want to use the pre-defined roles // below. type Role struct { - Name string - Site []Permission + Name string `json:"name"` + Site []Permission `json:"site"` // Org is a map of orgid to permissions. We represent orgid as a string. // TODO: Maybe switch to uuid, but tokens might need to support a "wildcard" org // which could be a special uuid (like all 0s?) - Org map[string][]Permission - User []Permission + Org map[string][]Permission `json:"org"` + User []Permission `json:"user"` } // Roles are stored as structs, so they can be serialized and stored. Until we store them elsewhere, @@ -34,28 +32,28 @@ var ( // RoleAdmin is a role that allows everything everywhere. RoleAdmin = Role{ Name: "admin", - Site: permissions(map[ResourceType][]Action{ - Wildcard: {Wildcard}, + Site: permissions(map[Object][]Action{ + ResourceWildcard: {WildcardSymbol}, }), } // RoleMember is a role that allows access to user-level resources. RoleMember = Role{ Name: "member", - User: permissions(map[ResourceType][]Action{ - Wildcard: {Wildcard}, + User: permissions(map[Object][]Action{ + ResourceWildcard: {WildcardSymbol}, }), } // RoleAuditor is an example on how to give more precise permissions RoleAuditor = Role{ Name: "auditor", - Site: permissions(map[ResourceType][]Action{ + Site: permissions(map[Object][]Action{ // TODO: @emyrk when audit logs are added, add back a read perm //ResourceAuditLogs: {ActionRead}, // Should be able to read user details to associate with logs. // Without this the user-id in logs is not very helpful - ResourceUser: {ActionRead}, + ResourceWorkspace: {ActionRead}, }), } ) @@ -110,10 +108,12 @@ func RoleOrgMember(orgID string) Role { func RoleWorkspaceAgent(workspaceID string) Role { return Role{ Name: fmt.Sprintf("agent-%s", workspaceID), + // This is at the site level to prevent the token from losing access if the user + // is kicked from the org Site: []Permission{ { Negate: false, - ResourceType: ResourceWorkspace, + ResourceType: ResourceWorkspace.Type, ResourceID: workspaceID, Action: ActionRead, }, @@ -121,15 +121,15 @@ func RoleWorkspaceAgent(workspaceID string) Role { } } -func permissions(perms map[ResourceType][]Action) []Permission { +func permissions(perms map[Object][]Action) []Permission { list := make([]Permission, 0, len(perms)) for k, actions := range perms { for _, act := range actions { act := act list = append(list, Permission{ Negate: false, - ResourceType: k, - ResourceID: Wildcard, + ResourceType: k.Type, + ResourceID: WildcardSymbol, Action: act, }) } diff --git a/coderd/authz/subject.go b/coderd/authz/subject.go deleted file mode 100644 index 4d97280be3f8d..0000000000000 --- a/coderd/authz/subject.go +++ /dev/null @@ -1,27 +0,0 @@ -package authz - -// 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 - // GetRoles returns the list of roles for the given - GetRoles() ([]Role, error) -} - -// 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"` - Roles []Role -} - -func (s SubjectTODO) ID() string { - return s.UserID -} - -func (s SubjectTODO) GetRoles() ([]Role, error) { - return s.Roles, nil -} From 690d41db8ba949de997aa24586286b1f2d3ce91f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 19:11:50 -0500 Subject: [PATCH 61/75] Update go mod with opa --- go.mod | 11 +++++++++-- go.sum | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index bf86f67a78f8c..f601a2b2cc4d6 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,6 @@ require ( github.com/charmbracelet/charm v0.11.0 github.com/charmbracelet/lipgloss v0.5.0 github.com/cli/safeexec v1.0.0 - github.com/cloudflare/cloudflared v0.0.0-20220308214351-5352b3cf0489 github.com/coder/retry v1.3.0 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/creack/pty v1.1.18 @@ -97,6 +96,14 @@ require github.com/go-chi/httprate v0.5.3 require github.com/jedib0t/go-pretty/v6 v6.3.0 +require ( + github.com/OneOfOne/xxhash v1.2.8 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/open-policy-agent/opa v0.39.0 // indirect + github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect + github.com/yashtewari/glob-intersection v0.1.0 // indirect +) + require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/BurntSushi/toml v1.0.0 // indirect @@ -152,7 +159,7 @@ require ( github.com/gobwas/ws v1.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.7 // indirect diff --git a/go.sum b/go.sum index 861b43638d60f..ff644f9829b53 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= @@ -260,6 +262,7 @@ github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytecodealliance/wasmtime-go v0.35.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= @@ -423,6 +426,7 @@ github.com/coredns/coredns v1.9.0 h1:M1EF1uups4CYcQGb1z8A97mfoq4BYCw3+xCYcJkOSDc github.com/coredns/coredns v1.9.0/go.mod h1:czzy6Ofs15Mzn1PXpWoplBCZxoWdGoQUInL9uPSiYME= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= @@ -440,6 +444,7 @@ github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= @@ -466,6 +471,7 @@ github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xb github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= @@ -474,6 +480,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dhui/dktest v0.3.7/go.mod h1:nYMOkafiA07WchSwKnKFUSbGMb2hMm5DrCGiXYG6gwM= @@ -549,6 +556,7 @@ github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -556,6 +564,7 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= @@ -579,6 +588,7 @@ github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404Z github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -617,6 +627,7 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ini/ini v1.66.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -629,6 +640,9 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= @@ -781,6 +795,7 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/gomodule/redigo v1.7.0/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -872,6 +887,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.15.2/go.mod h1:vO11I9oWA+KsxmfFQPhLnnIb1VDE24M+pdxZFiuZcA8= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= @@ -1093,8 +1109,10 @@ github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= @@ -1207,6 +1225,7 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -1223,6 +1242,7 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyex github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.46 h1:uzwpxRtSVxtcIZmz/4Uz6/Rn7G11DvsaslXoy5LxQio= github.com/miekg/dns v1.1.46/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -1306,6 +1326,7 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -1334,6 +1355,8 @@ github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je4 github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/open-policy-agent/opa v0.39.0 h1:nus6g0UC4+6adN5GV2W7K/gsL9QoELBlv5iBVyLVzWI= +github.com/open-policy-agent/opa v0.39.0/go.mod h1:M+l9UHc2T3PCZ/RMPxiHNKZZhukiOWfmaSapx71TmGM= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1376,6 +1399,7 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap github.com/pelletier/go-toml/v2 v2.0.0-beta.6 h1:JFNqj2afbbhCqTiyN16D7Tudc/aaDzE2FBDk+VlBQnE= github.com/pelletier/go-toml/v2 v2.0.0-beta.6/go.mod h1:ke6xncR3W76Ba8xnVxkrZG0js6Rd2BsQEAYrfgJ6eQA= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= @@ -1490,6 +1514,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T github.com/quasilyte/go-ruleguard/dsl v0.3.19 h1:5+KTKb2YREUYiqZFEIuifFyBxlcCUPWgNZkWy71XS0Q= github.com/quasilyte/go-ruleguard/dsl v0.3.19/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -1578,6 +1604,7 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.8.1 h1:izYHOT71f9iZ7iq37Uqjael60/vYC6vMtzedudZ0zEk= @@ -1587,6 +1614,7 @@ github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= @@ -1600,6 +1628,7 @@ github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1658,6 +1687,7 @@ github.com/twitchtv/twirp v8.1.1+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vF github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/unrolled/secure v1.10.0 h1:TBNP42z2AB+2pW9PR6vdbqhlQuv1iTeSVzK1qHjOBzA= @@ -1710,6 +1740,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg= +github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1740,14 +1772,26 @@ go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3C go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0/go.mod h1:PFmBsWbldL1kiWZk9+0LBZz2brhByaGsvp6pRICMlPE= go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= +go.opentelemetry.io/otel v1.6.0/go.mod h1:bfJD2DZVw0LBxghOTlgnlI0CV3hLDu9XF/QKOUXMTQQ= +go.opentelemetry.io/otel v1.6.1/go.mod h1:blzUabWHkX6LJewxvadmzafgh/wnvBSDBdOuwkAtrWQ= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.6.1/go.mod h1:NEu79Xo32iVb+0gVNV8PMd7GoWqnyDXRlj04yFjqz40= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.6.1/go.mod h1:YJ/JbY5ag/tSQFXzH3mtDmHqzF3aFn3DI/aB1n7pt4w= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.6.1/go.mod h1:UJJXJj0rltNIemDMwkOJyggsvyMG9QHfJeFH0HS5JjM= +go.opentelemetry.io/otel/metric v0.28.0/go.mod h1:TrzsfQAmQaB1PDcdhBauLMk7nyyg9hm+GoQq/ekE9Iw= +go.opentelemetry.io/otel/sdk v1.6.1/go.mod h1:IVYrddmFZ+eJqu2k38qD3WezFR2pymCzm8tdxyh3R4E= +go.opentelemetry.io/otel/trace v1.6.0/go.mod h1:qs7BrU5cZ8dXQHBGxHMOxwME/27YH2qEp4/+tZLLwJE= +go.opentelemetry.io/otel/trace v1.6.1/go.mod h1:RkFRM1m0puWIq10oxImnGEduNBzxiN7TXluRBtE+5j0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.12.1/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -1765,6 +1809,7 @@ golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -1934,6 +1979,7 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1989,6 +2035,7 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2077,6 +2124,7 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2140,6 +2188,7 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -2410,6 +2459,7 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= From fae331454ddfc0d5dc4de4749015fce9bb4a36cd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 19:12:10 -0500 Subject: [PATCH 62/75] Go mod tidy --- go.mod | 7 +++++-- go.sum | 9 ++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f601a2b2cc4d6..f28ae7f23c703 100644 --- a/go.mod +++ b/go.mod @@ -94,12 +94,15 @@ require ( require github.com/go-chi/httprate v0.5.3 -require github.com/jedib0t/go-pretty/v6 v6.3.0 +require ( + github.com/cloudflare/cloudflared v0.0.0-00010101000000-000000000000 + github.com/jedib0t/go-pretty/v6 v6.3.0 + github.com/open-policy-agent/opa v0.39.0 +) require ( github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/ghodss/yaml v1.0.0 // indirect - github.com/open-policy-agent/opa v0.39.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect ) diff --git a/go.sum b/go.sum index ff644f9829b53..51a42f1b76736 100644 --- a/go.sum +++ b/go.sum @@ -262,6 +262,7 @@ github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytecodealliance/wasmtime-go v0.35.0 h1:VZjaZ0XOY0qp9TQfh0CQj9zl/AbdeXePVTALy8V1sKs= github.com/bytecodealliance/wasmtime-go v0.35.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= @@ -271,6 +272,7 @@ github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -471,6 +473,7 @@ github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xb github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= @@ -478,8 +481,8 @@ github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11 github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -563,7 +566,9 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8= github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= @@ -791,11 +796,13 @@ github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8l github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.7.0/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= From 856a933dcf0d5fc575839d6b79f8de4d8bc4fe0e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 19:17:11 -0500 Subject: [PATCH 63/75] Add rego comments --- coderd/authz/policy.rego | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/coderd/authz/policy.rego b/coderd/authz/policy.rego index cbebe3ff52cfb..0f4a5011b25b9 100644 --- a/coderd/authz/policy.rego +++ b/coderd/authz/policy.rego @@ -2,8 +2,10 @@ package authz import future.keywords.in import future.keywords.every -# https://play.openpolicyagent.org/p/Jlzq5gIjkd -# grant_allow: https://play.openpolicyagent.org/p/9wpE9x6Tg2 +# A great playground: https://play.openpolicyagent.org/ +# TODO: Add debug instructions to do in the cli. Running really short on time, the +# playground is sufficient for now imo. In the future we can provide a tidy bash +# script for running this against predefined input. # bool_flip lets you assign a value to an inverted bool. # You cannot do 'x := !false', but you can do 'x := bool_flip(false)' @@ -38,8 +40,10 @@ perms_grant(permissions) = grants { # result is the default value. default site = {} site = grant { - # Boolean set for all site wide permissions - grant = { v | + # Boolean set for all site wide permissions. + grant = { v | # Use set comprehension to remove dulpicate values + # For each role, grab the site permission. + # Find the grants on this permission list. v = perms_grant(input.subject.roles[_].site)[_] } } @@ -49,13 +53,16 @@ user = grant { # Only apply user permissions if the user owns the resource input.object.owner != "" input.object.owner == input.subject.id - grant = { v | + grant = { v | # Use set comprehension to remove dulpicate values + # For each role, grab the user permissions. + # Find the grants on this permission list. v = perms_grant(input.subject.roles[_].user)[_] } } # Organizations are more complex. If the user has no roles that specifically indicate the org_id of the object, # then we want to block the action. This is because that means the user is not a member of the org. +# A non-member cannot access any org resources. # org_member returns the set of permissions associated with a user if the user is a member of the # organization @@ -94,6 +101,8 @@ org = set { # The allow block is quite simple. Any set with `false` cascades down in levels. # Authorization looks for any `allow` statement that is true. Multiple can be true! +# Note that the absense of `allow` means "unauthorized". +# An explicit `"allow": true` is required. # site allow allow { From e04810456228dc38ae4869a9b78be48bf2644105 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 19:22:08 -0500 Subject: [PATCH 64/75] Use cached rego query --- coderd/authz/authz.go | 63 +++++++++++++++++++++++++++++---- coderd/authz/authz_test.go | 4 ++- coderd/authz/example_test.go | 10 +++--- coderd/authz/rego.go | 68 ------------------------------------ 4 files changed, 65 insertions(+), 80 deletions(-) delete mode 100644 coderd/authz/rego.go diff --git a/coderd/authz/authz.go b/coderd/authz/authz.go index f15877cb56af4..aa1d88f20edf7 100644 --- a/coderd/authz/authz.go +++ b/coderd/authz/authz.go @@ -2,18 +2,67 @@ package authz import ( "context" + _ "embed" + "golang.org/x/xerrors" + + "github.com/open-policy-agent/opa/rego" ) -var ErrUnauthorized = xerrors.New("unauthorized") +// RegoAuthorizer will use a prepared rego query for performing authorize() +type RegoAuthorizer struct { + query rego.PreparedEvalQuery +} + +// Load the policy from policy.rego in this directory. +//go:embed policy.rego +var policy string + +func NewAuthorizer() (*RegoAuthorizer, error) { + ctx := context.Background() + query, err := rego.New( + // allowed is the `allow` field from the prepared query. This is the field to check if authorization is + // granted. + rego.Query("allowed = data.authz.allow"), + rego.Module("policy.rego", policy), + ).PrepareForEval(ctx) -// TODO: Implement Authorize. This will be implmented in mainly rego. -func Authorize(ctx context.Context, subjID string, roles []Role, obj Object, action Action) error { - // TODO: Cache authorizer - authorizer, err := newAuthorizer() if err != nil { - return ForbiddenWithInternal(xerrors.Errorf("new authorizer: %w", err), nil) + return nil, xerrors.Errorf("prepare query: %w", err) } + return &RegoAuthorizer{query: query}, nil +} + +type authSubject struct { + ID string `json:"id"` + Roles []Role `json:"roles"` + + SitePermissions []Permission `json:"site_permissions"` + OrgPermissions []Permission `json:"org_permissions"` + UserPermissions []Permission `json:"user_permissions"` +} - return authorizer.Authorize(ctx, subjID, roles, obj, action) +func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, object Object, action Action) error { + input := map[string]interface{}{ + "subject": authSubject{ + ID: subjectID, + Roles: roles, + }, + "object": object, + "action": action, + } + + results, err := a.query.Eval(ctx, rego.EvalInput(input)) + if err != nil { + return ForbiddenWithInternal(xerrors.Errorf("eval rego: %w, err"), input) + } + + if len(results) != 1 { + return ForbiddenWithInternal(xerrors.Errorf("expect only 1 result, got %d", len(results)), input) + } + + if results[0].Bindings["allowed"] != true { + return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input) + } + return nil } diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index d925f2b3cced4..153ae4c206070 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -590,11 +590,13 @@ type authTestCase struct { } func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTestCase) { + authorizer, err := authz.NewAuthorizer() + require.NoError(t, err) for _, cases := range sets { for _, c := range cases { t.Run(name, func(t *testing.T) { for _, a := range c.actions { - err := authz.Authorize(context.Background(), subject.UserID, subject.Roles, c.resource, a) + err := authorizer.Authorize(context.Background(), subject.UserID, subject.Roles, c.resource, a) if c.allow { if err != nil { var uerr *authz.Unauthorized diff --git a/coderd/authz/example_test.go b/coderd/authz/example_test.go index 1e4cc043c0d97..b04935a41a36c 100644 --- a/coderd/authz/example_test.go +++ b/coderd/authz/example_test.go @@ -14,6 +14,8 @@ func TestExample(t *testing.T) { t.Skip("TODO: unskip when rego is done") t.Parallel() ctx := context.Background() + authorizer, err := authz.NewAuthorizer() + require.NoError(t, err) // user will become an authn object, and can even be a database.User if it // fulfills the interface. Until then, use a placeholder. @@ -30,7 +32,7 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("ReadAllWorkspaces", func(t *testing.T) { // To read all workspaces on the site - err := authz.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.All(), authz.ActionRead) + err := authorizer.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.All(), authz.ActionRead) var _ = err // require.Error(t, err, "this user cannot read all workspaces") }) @@ -38,17 +40,17 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("ReadOrgWorkspaces", func(t *testing.T) { // To read all workspaces on the org 'default' - err := authz.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("default"), authz.ActionRead) + err := authorizer.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("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(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID), authz.ActionRead) + err := authorizer.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID), authz.ActionRead) require.NoError(t, err, "this user can their workspace") - err = authz.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234"), authz.ActionRead) + err = authorizer.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234"), authz.ActionRead) require.NoError(t, err, "this user can read workspace '1234'") }) } diff --git a/coderd/authz/rego.go b/coderd/authz/rego.go deleted file mode 100644 index c6e4ccbc3892c..0000000000000 --- a/coderd/authz/rego.go +++ /dev/null @@ -1,68 +0,0 @@ -package authz - -import ( - "context" - _ "embed" - - "golang.org/x/xerrors" - - "github.com/open-policy-agent/opa/rego" -) - -// regoAuthorizer will use a prepared rego query for performing authorize() -type regoAuthorizer struct { - query rego.PreparedEvalQuery -} - -// Load the policy from policy.rego in this directory. -//go:embed policy.rego -var policy string - -func newAuthorizer() (*regoAuthorizer, error) { - ctx := context.Background() - query, err := rego.New( - // allowed is the `allow` field from the prepared query. This is the field to check if authorization is - // granted. - rego.Query("allowed = data.authz.allow"), - rego.Module("policy.rego", policy), - ).PrepareForEval(ctx) - - if err != nil { - return nil, xerrors.Errorf("prepare query: %w", err) - } - return ®oAuthorizer{query: query}, nil -} - -type authSubject struct { - ID string `json:"id"` - Roles []Role `json:"roles"` - - SitePermissions []Permission `json:"site_permissions"` - OrgPermissions []Permission `json:"org_permissions"` - UserPermissions []Permission `json:"user_permissions"` -} - -func (a regoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, object Object, action Action) error { - input := map[string]interface{}{ - "subject": authSubject{ - ID: subjectID, - Roles: roles, - }, - "object": object, - "action": action, - } - - results, err := a.query.Eval(ctx, rego.EvalInput(input)) - if err != nil { - return ForbiddenWithInternal(xerrors.Errorf("eval rego: %w, err"), input) - } - - if len(results) != 1 { - return ForbiddenWithInternal(xerrors.Errorf("expect only 1 result, got %d", len(results)), input) - } - - if results[0].Bindings["allowed"] != true { - return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input) - } - return nil -} From adae3799383891f6b2741da1ca29a3b899d65943 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 19:26:50 -0500 Subject: [PATCH 65/75] Correct user-deny perm in test --- coderd/authz/authz_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go index 153ae4c206070..70969132b6d85 100644 --- a/coderd/authz/authz_test.go +++ b/coderd/authz/authz_test.go @@ -442,7 +442,7 @@ func TestAuthorizeLevels(t *testing.T) { { Name: "user-deny-all", // List out deny permissions explicitly - Site: []authz.Permission{ + User: []authz.Permission{ { Negate: true, ResourceType: authz.WildcardSymbol, @@ -516,7 +516,7 @@ func TestAuthorizeLevels(t *testing.T) { { Name: "user-deny-all", // List out deny permissions explicitly - Site: []authz.Permission{ + User: []authz.Permission{ { Negate: true, ResourceType: authz.WildcardSymbol, From 0e7f9fab8909a0e3d7317a6abefde9fa6add35de Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 22:30:10 -0500 Subject: [PATCH 66/75] tabs to spaces --- coderd/authz/policy.rego | 54 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/coderd/authz/policy.rego b/coderd/authz/policy.rego index 0f4a5011b25b9..6fe659296fed2 100644 --- a/coderd/authz/policy.rego +++ b/coderd/authz/policy.rego @@ -10,13 +10,13 @@ import future.keywords.every # bool_flip lets you assign a value to an inverted bool. # You cannot do 'x := !false', but you can do 'x := bool_flip(false)' bool_flip(b) = flipped { - b - flipped = false + b + flipped = false } bool_flip(b) = flipped { - not b - flipped = true + not b + flipped = true } # perms_grant returns a set of boolean values (true, false). @@ -24,12 +24,12 @@ bool_flip(b) = flipped { # resource_type, resource_id, and action. # The empty set is returned if no relevant permissions are found. perms_grant(permissions) = grants { - # If there are no permissions, this value is the empty set {}. - grants := { x | - # All permissions ... - perm := permissions[_] - # Such that the permission action, type, and resource_id matches - perm.action in [input.action, "*"] + # If there are no permissions, this value is the empty set {}. + grants := { x | + # All permissions ... + perm := permissions[_] + # Such that the permission action, type, and resource_id matches + perm.action in [input.action, "*"] perm.resource_type in [input.object.type, "*"] perm.resource_id in [input.object.id, "*"] x := bool_flip(perm.negate) @@ -40,23 +40,23 @@ perms_grant(permissions) = grants { # result is the default value. default site = {} site = grant { - # Boolean set for all site wide permissions. + # Boolean set for all site wide permissions. grant = { v | # Use set comprehension to remove dulpicate values # For each role, grab the site permission. # Find the grants on this permission list. - v = perms_grant(input.subject.roles[_].site)[_] + v = perms_grant(input.subject.roles[_].site)[_] } } default user = {} user = grant { - # Only apply user permissions if the user owns the resource + # Only apply user permissions if the user owns the resource input.object.owner != "" - input.object.owner == input.subject.id + input.object.owner == input.subject.id grant = { v | # Use set comprehension to remove dulpicate values # For each role, grab the user permissions. # Find the grants on this permission list. - v = perms_grant(input.subject.roles[_].user)[_] + v = perms_grant(input.subject.roles[_].user)[_] } } @@ -67,19 +67,19 @@ user = grant { # org_member returns the set of permissions associated with a user if the user is a member of the # organization org_member = grant { - input.object.org_owner != "" + input.object.org_owner != "" grant = { v | - v = perms_grant(input.subject.roles[_].org[input.object.org_owner])[_] + v = perms_grant(input.subject.roles[_].org[input.object.org_owner])[_] } } # If a user is not part of an organization, 'org_non_member' is set to true org_non_member { - input.object.org_owner != "" - # Identify if the user is in the org + input.object.org_owner != "" + # Identify if the user is in the org roles := input.subject.roles every role in roles { - not role.org[input.object.org_owner] + not role.org[input.object.org_owner] } } @@ -90,12 +90,12 @@ org_non_member { # It is important both rules cannot be true, as the `org` rules cannot produce multiple outputs. default org = [] org = set { - not org_non_member - set = org_member + not org_non_member + set = org_member } org = set { - org_non_member + org_non_member set = {false} } @@ -106,7 +106,7 @@ org = set { # site allow allow { - # No site wide deny + # No site wide deny not false in site # And all permissions are positive site[_] @@ -114,9 +114,9 @@ allow { # org allow allow { - # No site or org deny + # No site or org deny not false in site - not false in org + not false in org # And all permissions are positive org[_] } @@ -125,7 +125,7 @@ allow { allow { # No site, org, or user deny not false in site - not false in org + not false in org not false in user # And all permissions are positive user[_] From b1c7df46753ba3a8aa7f8d101185e7ce74fe9901 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 11 Apr 2022 22:31:41 -0500 Subject: [PATCH 67/75] Rename package to rbac --- coderd/authz/authz_test.go | 629 ------------------------- coderd/{authz => rbac}/README.md | 0 coderd/{authz => rbac}/action.go | 2 +- coderd/{authz => rbac}/authz.go | 2 +- coderd/rbac/authz_test.go | 628 ++++++++++++++++++++++++ coderd/{authz => rbac}/error.go | 2 +- coderd/{authz => rbac}/example_test.go | 20 +- coderd/{authz => rbac}/object.go | 2 +- coderd/{authz => rbac}/policy.rego | 0 coderd/{authz => rbac}/resources.go | 2 +- coderd/{authz => rbac}/role.go | 2 +- 11 files changed, 644 insertions(+), 645 deletions(-) delete mode 100644 coderd/authz/authz_test.go rename coderd/{authz => rbac}/README.md (100%) rename coderd/{authz => rbac}/action.go (93%) rename coderd/{authz => rbac}/authz.go (99%) create mode 100644 coderd/rbac/authz_test.go rename coderd/{authz => rbac}/error.go (98%) rename coderd/{authz => rbac}/example_test.go (64%) rename coderd/{authz => rbac}/object.go (98%) rename coderd/{authz => rbac}/policy.rego (100%) rename coderd/{authz => rbac}/resources.go (94%) rename coderd/{authz => rbac}/role.go (99%) diff --git a/coderd/authz/authz_test.go b/coderd/authz/authz_test.go deleted file mode 100644 index 70969132b6d85..0000000000000 --- a/coderd/authz/authz_test.go +++ /dev/null @@ -1,629 +0,0 @@ -package authz_test - -import ( - "context" - "encoding/json" - "golang.org/x/xerrors" - "testing" - - "github.com/coder/coder/coderd/authz" - "github.com/stretchr/testify/require" -) - -// subject is required because rego needs -type subject struct { - UserID string `json:"id"` - Roles []authz.Role `json:"roles"` -} - -// TestAuthorizeDomain test the very basic roles that are commonly used. -func TestAuthorizeDomain(t *testing.T) { - //t.Skip("TODO: unskip when rego is done") - t.Parallel() - defOrg := "default" - wrkID := "1234" - - user := subject{ - UserID: "me", - Roles: []authz.Role{authz.RoleMember, authz.RoleOrgMember(defOrg)}, - } - - testAuthorize(t, "Member", user, []authTestCase{ - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: true}, - - {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, - - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, - - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, - - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, - }) - - user = subject{ - UserID: "me", - Roles: []authz.Role{{ - Name: "deny-all", - // List out deny permissions explicitly - Site: []authz.Permission{ - { - Negate: true, - ResourceType: authz.WildcardSymbol, - ResourceID: authz.WildcardSymbol, - Action: authz.WildcardSymbol, - }, - }, - }}, - } - - testAuthorize(t, "DeletedMember", user, []authTestCase{ - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, - - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, - - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, - - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, - }) - - user = subject{ - UserID: "me", - Roles: []authz.Role{ - authz.RoleOrgAdmin(defOrg), - authz.RoleMember, - }, - } - - testAuthorize(t, "OrgAdmin", user, []authTestCase{ - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: true}, - - {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: true}, - - {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: false}, - - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, - - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: true}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, - - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, - - {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, - }) - - user = subject{ - UserID: "me", - Roles: []authz.Role{ - authz.RoleAdmin, - authz.RoleMember, - }, - } - - testAuthorize(t, "SiteAdmin", user, []authTestCase{ - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: true}, - - {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: true}, - - {resource: authz.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: true}, - - {resource: authz.ResourceWorkspace.All(), actions: allActions(), allow: true}, - - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true}, - - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: true}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true}, - - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true}, - - {resource: authz.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: true}, - }) - - // In practice this is a token scope on a regular subject - user = subject{ - UserID: "me", - Roles: []authz.Role{ - authz.RoleWorkspaceAgent(wrkID), - }, - } - - testAuthorize(t, "WorkspaceAgentToken", user, - // Read Actions - cases(func(c authTestCase) authTestCase { - c.actions = []authz.Action{authz.ActionRead} - return c - }, []authTestCase{ - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), allow: false}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.UserID), allow: false}, - - {resource: authz.ResourceWorkspace.WithID(wrkID), allow: true}, - - {resource: authz.ResourceWorkspace.All(), allow: false}, - - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, - - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, - - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, - - {resource: authz.ResourceWorkspace.WithID("not-id"), allow: false}, - }), - // Not read actions - cases(func(c authTestCase) authTestCase { - c.actions = []authz.Action{authz.ActionCreate, authz.ActionUpdate, authz.ActionDelete} - c.allow = false - return c - }, []authTestCase{ - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg)}, - - {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.WithOwner(user.UserID)}, - - {resource: authz.ResourceWorkspace.WithID(wrkID)}, - - {resource: authz.ResourceWorkspace.All()}, - - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg("other")}, - - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID)}, - {resource: authz.ResourceWorkspace.WithOwner("not-me")}, - - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me")}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id")}, - {resource: authz.ResourceWorkspace.InOrg("other")}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id")}, - {resource: authz.ResourceWorkspace.WithOwner("not-me")}, - - {resource: authz.ResourceWorkspace.WithID("not-id")}, - }), - ) - - // In practice this is a token scope on a regular subject - user = subject{ - UserID: "me", - Roles: []authz.Role{ - { - Name: "ReadOnlyOrgAndUser", - Site: []authz.Permission{}, - Org: map[string][]authz.Permission{ - defOrg: {{ - Negate: false, - ResourceType: "*", - ResourceID: "*", - Action: authz.ActionRead, - }}, - }, - User: []authz.Permission{ - { - Negate: false, - ResourceType: "*", - ResourceID: "*", - Action: authz.ActionRead, - }, - }, - }, - }, - } - - testAuthorize(t, "ReadOnly", user, - cases(func(c authTestCase) authTestCase { - c.actions = []authz.Action{authz.ActionRead} - return c - }, []authTestCase{ - // Read - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg), allow: true}, - - {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.WithOwner(user.UserID), allow: true}, - - {resource: authz.ResourceWorkspace.WithID(wrkID), allow: false}, - - {resource: authz.ResourceWorkspace.All(), allow: false}, - - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, - - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: true}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, - - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, - - {resource: authz.ResourceWorkspace.WithID("not-id"), allow: false}, - }), - - // Pass non-read actions - cases(func(c authTestCase) authTestCase { - c.actions = []authz.Action{authz.ActionCreate, authz.ActionUpdate, authz.ActionDelete} - c.allow = false - return c - }, []authTestCase{ - // Read - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg)}, - - {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.WithOwner(user.UserID)}, - - {resource: authz.ResourceWorkspace.WithID(wrkID)}, - - {resource: authz.ResourceWorkspace.All()}, - - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg("other")}, - - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID)}, - {resource: authz.ResourceWorkspace.WithOwner("not-me")}, - - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me")}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id")}, - {resource: authz.ResourceWorkspace.InOrg("other")}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id")}, - {resource: authz.ResourceWorkspace.WithOwner("not-me")}, - - {resource: authz.ResourceWorkspace.WithID("not-id")}, - })) -} - -// TestAuthorizeLevels ensures level overrides are acting appropriately -func TestAuthorizeLevels(t *testing.T) { - defOrg := "default" - wrkID := "1234" - - user := subject{ - UserID: "me", - Roles: []authz.Role{ - authz.RoleAdmin, - authz.RoleOrgDenyAll(defOrg), - { - Name: "user-deny-all", - // List out deny permissions explicitly - User: []authz.Permission{ - { - Negate: true, - ResourceType: authz.WildcardSymbol, - ResourceID: authz.WildcardSymbol, - Action: authz.WildcardSymbol, - }, - }, - }, - }, - } - - testAuthorize(t, "AdminAlwaysAllow", user, - cases(func(c authTestCase) authTestCase { - c.actions = allActions() - c.allow = true - return c - }, []authTestCase{ - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg)}, - - {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.WithOwner(user.UserID)}, - - {resource: authz.ResourceWorkspace.WithID(wrkID)}, - - {resource: authz.ResourceWorkspace.All()}, - - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg("other")}, - - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID)}, - {resource: authz.ResourceWorkspace.WithOwner("not-me")}, - - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me")}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id")}, - {resource: authz.ResourceWorkspace.InOrg("other")}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id")}, - {resource: authz.ResourceWorkspace.WithOwner("not-me")}, - - {resource: authz.ResourceWorkspace.WithID("not-id")}, - })) - - user = subject{ - UserID: "me", - Roles: []authz.Role{ - { - Name: "site-noise", - Site: []authz.Permission{ - { - Negate: true, - ResourceType: "random", - ResourceID: authz.WildcardSymbol, - Action: authz.WildcardSymbol, - }, - }, - }, - authz.RoleOrgAdmin(defOrg), - { - Name: "user-deny-all", - // List out deny permissions explicitly - User: []authz.Permission{ - { - Negate: true, - ResourceType: authz.WildcardSymbol, - ResourceID: authz.WildcardSymbol, - Action: authz.WildcardSymbol, - }, - }, - }, - }, - } - - testAuthorize(t, "OrgAllowAll", user, - cases(func(c authTestCase) authTestCase { - c.actions = allActions() - return c - }, []authTestCase{ - // Org + me + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg), allow: true}, - - {resource: authz.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner(user.UserID), allow: false}, - - {resource: authz.ResourceWorkspace.WithID(wrkID), allow: false}, - - {resource: authz.ResourceWorkspace.All(), allow: false}, - - // Other org + me + id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, - - // Other org + other user + id - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true}, - {resource: authz.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: true}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, - - // Other org + other use + other id - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.InOrg("other"), allow: false}, - - {resource: authz.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false}, - {resource: authz.ResourceWorkspace.WithOwner("not-me"), allow: false}, - - {resource: authz.ResourceWorkspace.WithID("not-id"), allow: false}, - })) -} - -// cases applies a given function to all test cases. This makes generalities easier to create. -func cases(opt func(c authTestCase) authTestCase, cases []authTestCase) []authTestCase { - if opt == nil { - return cases - } - for i := range cases { - cases[i] = opt(cases[i]) - } - return cases -} - -type authTestCase struct { - resource authz.Object - actions []authz.Action - allow bool -} - -func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTestCase) { - authorizer, err := authz.NewAuthorizer() - require.NoError(t, err) - for _, cases := range sets { - for _, c := range cases { - t.Run(name, func(t *testing.T) { - for _, a := range c.actions { - err := authorizer.Authorize(context.Background(), subject.UserID, subject.Roles, c.resource, a) - if c.allow { - if err != nil { - var uerr *authz.Unauthorized - xerrors.As(err, &uerr) - d, _ := json.Marshal(uerr.Input()) - t.Log(string(d)) - } - require.NoError(t, err, "expected no error for testcase action %s", a) - continue - } - - if err == nil { - d, _ := json.Marshal(map[string]interface{}{ - "subject": subject, - "object": c.resource, - "action": a, - }) - t.Log(string(d)) - } - require.Error(t, err, "expected unauthorized") - } - }) - } - } -} - -// allActions is a helper function to return all the possible actions types. -func allActions() []authz.Action { - return []authz.Action{authz.ActionCreate, authz.ActionRead, authz.ActionUpdate, authz.ActionDelete} -} diff --git a/coderd/authz/README.md b/coderd/rbac/README.md similarity index 100% rename from coderd/authz/README.md rename to coderd/rbac/README.md diff --git a/coderd/authz/action.go b/coderd/rbac/action.go similarity index 93% rename from coderd/authz/action.go rename to coderd/rbac/action.go index eb7360ef1d6c9..224047808e15e 100644 --- a/coderd/authz/action.go +++ b/coderd/rbac/action.go @@ -1,4 +1,4 @@ -package authz +package rbac // Action represents the allowed actions to be done on an object. type Action string diff --git a/coderd/authz/authz.go b/coderd/rbac/authz.go similarity index 99% rename from coderd/authz/authz.go rename to coderd/rbac/authz.go index aa1d88f20edf7..ba6b1d819dfae 100644 --- a/coderd/authz/authz.go +++ b/coderd/rbac/authz.go @@ -1,4 +1,4 @@ -package authz +package rbac import ( "context" diff --git a/coderd/rbac/authz_test.go b/coderd/rbac/authz_test.go new file mode 100644 index 0000000000000..1dcdc05110240 --- /dev/null +++ b/coderd/rbac/authz_test.go @@ -0,0 +1,628 @@ +package rbac_test + +import ( + "context" + "encoding/json" + "golang.org/x/xerrors" + "testing" + + "github.com/coder/coder/coderd/rbac" + "github.com/stretchr/testify/require" +) + +// subject is required because rego needs +type subject struct { + UserID string `json:"id"` + Roles []rbac.Role `json:"roles"` +} + +// TestAuthorizeDomain test the very basic roles that are commonly used. +func TestAuthorizeDomain(t *testing.T) { + t.Parallel() + defOrg := "default" + wrkID := "1234" + + user := subject{ + UserID: "me", + Roles: []rbac.Role{rbac.RoleMember, rbac.RoleOrgMember(defOrg)}, + } + + testAuthorize(t, "Member", user, []authTestCase{ + // Org + me + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: true}, + + {resource: rbac.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: false}, + + // Other org + me + id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, + + // Other org + other user + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, + + // Other org + other use + other id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, + }) + + user = subject{ + UserID: "me", + Roles: []rbac.Role{{ + Name: "deny-all", + // List out deny permissions explicitly + Site: []rbac.Permission{ + { + Negate: true, + ResourceType: rbac.WildcardSymbol, + ResourceID: rbac.WildcardSymbol, + Action: rbac.WildcardSymbol, + }, + }, + }}, + } + + testAuthorize(t, "DeletedMember", user, []authTestCase{ + // Org + me + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: false}, + + // Other org + me + id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, + + // Other org + other user + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, + + // Other org + other use + other id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, + }) + + user = subject{ + UserID: "me", + Roles: []rbac.Role{ + rbac.RoleOrgAdmin(defOrg), + rbac.RoleMember, + }, + } + + testAuthorize(t, "OrgAdmin", user, []authTestCase{ + // Org + me + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: true}, + + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: true}, + + {resource: rbac.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: false}, + + // Other org + me + id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, + + // Other org + other user + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: true}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, + + // Other org + other use + other id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false}, + + {resource: rbac.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: false}, + }) + + user = subject{ + UserID: "me", + Roles: []rbac.Role{ + rbac.RoleAdmin, + rbac.RoleMember, + }, + } + + testAuthorize(t, "SiteAdmin", user, []authTestCase{ + // Org + me + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: true}, + + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID), actions: allActions(), allow: true}, + + {resource: rbac.ResourceWorkspace.WithID(wrkID), actions: allActions(), allow: true}, + + {resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: true}, + + // Other org + me + id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true}, + + // Other org + other user + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: true}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true}, + + // Other org + other use + other id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true}, + + {resource: rbac.ResourceWorkspace.WithID("not-id"), actions: allActions(), allow: true}, + }) + + // In practice this is a token scope on a regular subject + user = subject{ + UserID: "me", + Roles: []rbac.Role{ + rbac.RoleWorkspaceAgent(wrkID), + }, + } + + testAuthorize(t, "WorkspaceAgentToken", user, + // Read Actions + cases(func(c authTestCase) authTestCase { + c.actions = []rbac.Action{rbac.ActionRead} + return c + }, []authTestCase{ + // Org + me + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID), allow: false}, + + {resource: rbac.ResourceWorkspace.WithID(wrkID), allow: true}, + + {resource: rbac.ResourceWorkspace.All(), allow: false}, + + // Other org + me + id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg("other"), allow: false}, + + // Other org + other user + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false}, + + // Other org + other use + other id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false}, + + {resource: rbac.ResourceWorkspace.WithID("not-id"), allow: false}, + }), + // Not read actions + cases(func(c authTestCase) authTestCase { + c.actions = []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete} + c.allow = false + return c + }, []authTestCase{ + // Org + me + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg)}, + + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID)}, + + {resource: rbac.ResourceWorkspace.WithID(wrkID)}, + + {resource: rbac.ResourceWorkspace.All()}, + + // Other org + me + id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg("other")}, + + // Other org + other user + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me")}, + + // Other org + other use + other id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me")}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id")}, + {resource: rbac.ResourceWorkspace.InOrg("other")}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id")}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me")}, + + {resource: rbac.ResourceWorkspace.WithID("not-id")}, + }), + ) + + // In practice this is a token scope on a regular subject + user = subject{ + UserID: "me", + Roles: []rbac.Role{ + { + Name: "ReadOnlyOrgAndUser", + Site: []rbac.Permission{}, + Org: map[string][]rbac.Permission{ + defOrg: {{ + Negate: false, + ResourceType: "*", + ResourceID: "*", + Action: rbac.ActionRead, + }}, + }, + User: []rbac.Permission{ + { + Negate: false, + ResourceType: "*", + ResourceID: "*", + Action: rbac.ActionRead, + }, + }, + }, + }, + } + + testAuthorize(t, "ReadOnly", user, + cases(func(c authTestCase) authTestCase { + c.actions = []rbac.Action{rbac.ActionRead} + return c + }, []authTestCase{ + // Read + // Org + me + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg), allow: true}, + + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID), allow: true}, + + {resource: rbac.ResourceWorkspace.WithID(wrkID), allow: false}, + + {resource: rbac.ResourceWorkspace.All(), allow: false}, + + // Other org + me + id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), allow: false}, + + // Other org + other user + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: true}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false}, + + // Other org + other use + other id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false}, + + {resource: rbac.ResourceWorkspace.WithID("not-id"), allow: false}, + }), + + // Pass non-read actions + cases(func(c authTestCase) authTestCase { + c.actions = []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete} + c.allow = false + return c + }, []authTestCase{ + // Read + // Org + me + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg)}, + + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID)}, + + {resource: rbac.ResourceWorkspace.WithID(wrkID)}, + + {resource: rbac.ResourceWorkspace.All()}, + + // Other org + me + id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg("other")}, + + // Other org + other user + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me")}, + + // Other org + other use + other id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me")}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id")}, + {resource: rbac.ResourceWorkspace.InOrg("other")}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id")}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me")}, + + {resource: rbac.ResourceWorkspace.WithID("not-id")}, + })) +} + +// TestAuthorizeLevels ensures level overrides are acting appropriately +func TestAuthorizeLevels(t *testing.T) { + defOrg := "default" + wrkID := "1234" + + user := subject{ + UserID: "me", + Roles: []rbac.Role{ + rbac.RoleAdmin, + rbac.RoleOrgDenyAll(defOrg), + { + Name: "user-deny-all", + // List out deny permissions explicitly + User: []rbac.Permission{ + { + Negate: true, + ResourceType: rbac.WildcardSymbol, + ResourceID: rbac.WildcardSymbol, + Action: rbac.WildcardSymbol, + }, + }, + }, + }, + } + + testAuthorize(t, "AdminAlwaysAllow", user, + cases(func(c authTestCase) authTestCase { + c.actions = allActions() + c.allow = true + return c + }, []authTestCase{ + // Org + me + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg)}, + + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID)}, + + {resource: rbac.ResourceWorkspace.WithID(wrkID)}, + + {resource: rbac.ResourceWorkspace.All()}, + + // Other org + me + id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg("other")}, + + // Other org + other user + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID(wrkID)}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me")}, + + // Other org + other use + other id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me")}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id")}, + {resource: rbac.ResourceWorkspace.InOrg("other")}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id")}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me")}, + + {resource: rbac.ResourceWorkspace.WithID("not-id")}, + })) + + user = subject{ + UserID: "me", + Roles: []rbac.Role{ + { + Name: "site-noise", + Site: []rbac.Permission{ + { + Negate: true, + ResourceType: "random", + ResourceID: rbac.WildcardSymbol, + Action: rbac.WildcardSymbol, + }, + }, + }, + rbac.RoleOrgAdmin(defOrg), + { + Name: "user-deny-all", + // List out deny permissions explicitly + User: []rbac.Permission{ + { + Negate: true, + ResourceType: rbac.WildcardSymbol, + ResourceID: rbac.WildcardSymbol, + Action: rbac.WildcardSymbol, + }, + }, + }, + }, + } + + testAuthorize(t, "OrgAllowAll", user, + cases(func(c authTestCase) authTestCase { + c.actions = allActions() + return c + }, []authTestCase{ + // Org + me + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID).WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg), allow: true}, + + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID).WithID(wrkID), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner(user.UserID), allow: false}, + + {resource: rbac.ResourceWorkspace.WithID(wrkID), allow: false}, + + {resource: rbac.ResourceWorkspace.All(), allow: false}, + + // Other org + me + id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), allow: false}, + + // Other org + other user + id + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true}, + {resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: true}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID(wrkID), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false}, + + // Other org + other use + other id + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false}, + {resource: rbac.ResourceWorkspace.InOrg("other"), allow: false}, + + {resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false}, + {resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false}, + + {resource: rbac.ResourceWorkspace.WithID("not-id"), allow: false}, + })) +} + +// cases applies a given function to all test cases. This makes generalities easier to create. +func cases(opt func(c authTestCase) authTestCase, cases []authTestCase) []authTestCase { + if opt == nil { + return cases + } + for i := range cases { + cases[i] = opt(cases[i]) + } + return cases +} + +type authTestCase struct { + resource rbac.Object + actions []rbac.Action + allow bool +} + +func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTestCase) { + authorizer, err := rbac.NewAuthorizer() + require.NoError(t, err) + for _, cases := range sets { + for _, c := range cases { + t.Run(name, func(t *testing.T) { + for _, a := range c.actions { + err := authorizer.Authorize(context.Background(), subject.UserID, subject.Roles, c.resource, a) + if c.allow { + if err != nil { + var uerr *rbac.Unauthorized + xerrors.As(err, &uerr) + d, _ := json.Marshal(uerr.Input()) + t.Log(string(d)) + } + require.NoError(t, err, "expected no error for testcase action %s", a) + continue + } + + if err == nil { + d, _ := json.Marshal(map[string]interface{}{ + "subject": subject, + "object": c.resource, + "action": a, + }) + t.Log(string(d)) + } + require.Error(t, err, "expected unauthorized") + } + }) + } + } +} + +// allActions is a helper function to return all the possible actions types. +func allActions() []rbac.Action { + return []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete} +} diff --git a/coderd/authz/error.go b/coderd/rbac/error.go similarity index 98% rename from coderd/authz/error.go rename to coderd/rbac/error.go index 454b88ebdf1be..3beafeef30f23 100644 --- a/coderd/authz/error.go +++ b/coderd/rbac/error.go @@ -1,4 +1,4 @@ -package authz +package rbac const ( // UnauthorizedErrorMessage is the error message that should be returned to diff --git a/coderd/authz/example_test.go b/coderd/rbac/example_test.go similarity index 64% rename from coderd/authz/example_test.go rename to coderd/rbac/example_test.go index b04935a41a36c..e093ff203e237 100644 --- a/coderd/authz/example_test.go +++ b/coderd/rbac/example_test.go @@ -1,10 +1,10 @@ -package authz_test +package rbac_test import ( "context" "testing" - "github.com/coder/coder/coderd/authz" + "github.com/coder/coder/coderd/rbac" "github.com/stretchr/testify/require" ) @@ -14,16 +14,16 @@ func TestExample(t *testing.T) { t.Skip("TODO: unskip when rego is done") t.Parallel() ctx := context.Background() - authorizer, err := authz.NewAuthorizer() + authorizer, err := rbac.NewAuthorizer() require.NoError(t, err) // user will become an authn object, and can even be a database.User if it // fulfills the interface. Until then, use a placeholder. user := subject{ UserID: "alice", - Roles: []authz.Role{ - authz.RoleOrgAdmin("default"), - authz.RoleMember, + Roles: []rbac.Role{ + rbac.RoleOrgAdmin("default"), + rbac.RoleMember, }, } @@ -32,7 +32,7 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("ReadAllWorkspaces", func(t *testing.T) { // To read all workspaces on the site - err := authorizer.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.All(), authz.ActionRead) + err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ResourceWorkspace.All(), rbac.ActionRead) var _ = err // require.Error(t, err, "this user cannot read all workspaces") }) @@ -40,17 +40,17 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("ReadOrgWorkspaces", func(t *testing.T) { // To read all workspaces on the org 'default' - err := authorizer.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("default"), authz.ActionRead) + err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ResourceWorkspace.InOrg("default"), rbac.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 := authorizer.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID), authz.ActionRead) + err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ResourceWorkspace.InOrg("default").WithOwner(user.UserID), rbac.ActionRead) require.NoError(t, err, "this user can their workspace") - err = authorizer.Authorize(ctx, user.UserID, user.Roles, authz.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234"), authz.ActionRead) + err = authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234"), rbac.ActionRead) require.NoError(t, err, "this user can read workspace '1234'") }) } diff --git a/coderd/authz/object.go b/coderd/rbac/object.go similarity index 98% rename from coderd/authz/object.go rename to coderd/rbac/object.go index dd69b81dc7897..f312f5dbe7f37 100644 --- a/coderd/authz/object.go +++ b/coderd/rbac/object.go @@ -1,4 +1,4 @@ -package authz +package rbac // Object is used to create objects for authz checks when you have none in // hand to run the check on. diff --git a/coderd/authz/policy.rego b/coderd/rbac/policy.rego similarity index 100% rename from coderd/authz/policy.rego rename to coderd/rbac/policy.rego diff --git a/coderd/authz/resources.go b/coderd/rbac/resources.go similarity index 94% rename from coderd/authz/resources.go rename to coderd/rbac/resources.go index 1abeb6608957a..b8f79fb947b35 100644 --- a/coderd/authz/resources.go +++ b/coderd/rbac/resources.go @@ -1,4 +1,4 @@ -package authz +package rbac const WildcardSymbol = "*" diff --git a/coderd/authz/role.go b/coderd/rbac/role.go similarity index 99% rename from coderd/authz/role.go rename to coderd/rbac/role.go index baabe64f6d364..c25140aaf24cf 100644 --- a/coderd/authz/role.go +++ b/coderd/rbac/role.go @@ -1,4 +1,4 @@ -package authz +package rbac import "fmt" From 35574ad05a83ae63c4056892a8dd5294d66c3679 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 12 Apr 2022 08:23:15 -0500 Subject: [PATCH 68/75] run golangci-lint and goimports --- coderd/rbac/authz.go | 8 ++++++- coderd/rbac/authz_test.go | 9 +++++--- coderd/rbac/error.go | 20 ++++++++--------- coderd/rbac/example_test.go | 3 ++- coderd/rbac/object.go | 43 ++++++++++++++++++++++++------------- 5 files changed, 53 insertions(+), 30 deletions(-) diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index ba6b1d819dfae..66b30c98e2a0a 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -61,8 +61,14 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [ return ForbiddenWithInternal(xerrors.Errorf("expect only 1 result, got %d", len(results)), input) } - if results[0].Bindings["allowed"] != true { + allowedResult, ok := (results[0].Bindings["allowed"]).(bool) + if !ok { + return ForbiddenWithInternal(xerrors.Errorf("expected allowed to be a bool but got %T", allowedResult), input) + } + + if allowedResult { return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input) } + return nil } diff --git a/coderd/rbac/authz_test.go b/coderd/rbac/authz_test.go index 1dcdc05110240..0dd24451617b5 100644 --- a/coderd/rbac/authz_test.go +++ b/coderd/rbac/authz_test.go @@ -3,11 +3,13 @@ package rbac_test import ( "context" "encoding/json" - "golang.org/x/xerrors" "testing" - "github.com/coder/coder/coderd/rbac" + "golang.org/x/xerrors" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/rbac" ) // subject is required because rego needs @@ -429,6 +431,7 @@ func TestAuthorizeDomain(t *testing.T) { } // TestAuthorizeLevels ensures level overrides are acting appropriately +//nolint:paralleltest func TestAuthorizeLevels(t *testing.T) { defOrg := "default" wrkID := "1234" @@ -598,7 +601,7 @@ func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTes err := authorizer.Authorize(context.Background(), subject.UserID, subject.Roles, c.resource, a) if c.allow { if err != nil { - var uerr *rbac.Unauthorized + var uerr *rbac.UnauthorizedError xerrors.As(err, &uerr) d, _ := json.Marshal(uerr.Input()) t.Log(string(d)) diff --git a/coderd/rbac/error.go b/coderd/rbac/error.go index 3beafeef30f23..7d86697e41c3c 100644 --- a/coderd/rbac/error.go +++ b/coderd/rbac/error.go @@ -1,14 +1,14 @@ package rbac const ( - // UnauthorizedErrorMessage is the error message that should be returned to + // errUnauthorized is the error message that should be returned to // clients when an action is forbidden. It is intentionally vague to prevent // disclosing information that a client should not have access to. - UnauthorizedErrorMessage = "unauthorized" + errUnauthorized = "unauthorized" ) -// Unauthorized is the error type for authorization errors -type Unauthorized struct { +// UnauthorizedError is the error type for authorization errors +type UnauthorizedError struct { // internal is the internal error that should never be shown to the client. // It is only for debugging purposes. internal error @@ -18,26 +18,26 @@ type Unauthorized struct { // ForbiddenWithInternal creates a new error that will return a simple // "forbidden" to the client, logging internally the more detailed message // provided. -func ForbiddenWithInternal(internal error, input map[string]interface{}) *Unauthorized { +func ForbiddenWithInternal(internal error, input map[string]interface{}) *UnauthorizedError { if input == nil { input = map[string]interface{}{} } - return &Unauthorized{ + return &UnauthorizedError{ internal: internal, input: input, } } // Error implements the error interface. -func (e *Unauthorized) Error() string { - return UnauthorizedErrorMessage +func (UnauthorizedError) Error() string { + return errUnauthorized } // Internal allows the internal error message to be logged. -func (e *Unauthorized) Internal() error { +func (e *UnauthorizedError) Internal() error { return e.internal } -func (e *Unauthorized) Input() map[string]interface{} { +func (e *UnauthorizedError) Input() map[string]interface{} { return e.input } diff --git a/coderd/rbac/example_test.go b/coderd/rbac/example_test.go index e093ff203e237..56b47e89f71fd 100644 --- a/coderd/rbac/example_test.go +++ b/coderd/rbac/example_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/coder/coder/coderd/rbac" "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/rbac" ) // TestExample gives some examples on how to use the authz library. diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index f312f5dbe7f37..b82b05b11fc5d 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -16,29 +16,42 @@ type Object struct { // TODO: SharedUsers? } +// All returns an object matching all resources of the same type. func (z Object) All() Object { - z.OrgID = "" - z.Owner = "" - z.ResourceID = "" - return z + return Object{ + ResourceID: "", + Owner: "", + OrgID: "", + Type: z.Type, + } } // InOrg adds an org OwnerID to the resource -//nolint:revive func (z Object) InOrg(orgID string) Object { - z.OrgID = orgID - return z + return Object{ + ResourceID: z.ResourceID, + Owner: z.Owner, + OrgID: orgID, + Type: z.Type, + } } // WithOwner adds an OwnerID to the resource -//nolint:revive -func (z Object) WithOwner(id string) Object { - z.Owner = id - return z +func (z Object) WithOwner(ownerID string) Object { + return Object{ + ResourceID: z.ResourceID, + Owner: ownerID, + OrgID: z.OrgID, + Type: z.Type, + } } -//nolint:revive -func (z Object) WithID(id string) Object { - z.ResourceID = id - return z +// WithID adds a ResourceID to the resource +func (z Object) WithID(resourceID string) Object { + return Object{ + ResourceID: resourceID, + Owner: z.Owner, + OrgID: z.OrgID, + Type: z.Type, + } } From 8a169472fd59cd0a633ba12155a8022497b160e4 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 12 Apr 2022 08:34:33 -0500 Subject: [PATCH 69/75] authz_test.go: log internal error --- coderd/rbac/authz_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/rbac/authz_test.go b/coderd/rbac/authz_test.go index 0dd24451617b5..35b5118e6c397 100644 --- a/coderd/rbac/authz_test.go +++ b/coderd/rbac/authz_test.go @@ -604,7 +604,8 @@ func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTes var uerr *rbac.UnauthorizedError xerrors.As(err, &uerr) d, _ := json.Marshal(uerr.Input()) - t.Log(string(d)) + t.Logf("input: %s", string(d)) + t.Logf("internal error: %+v", uerr.Internal().Error()) } require.NoError(t, err, "expected no error for testcase action %s", a) continue From 2969fc10a67d88d4c58788d224bb353e7183332b Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 12 Apr 2022 09:15:39 -0500 Subject: [PATCH 70/75] fixup! run golangci-lint and goimports --- coderd/rbac/authz.go | 10 +++++----- coderd/rbac/authz_test.go | 1 + coderd/rbac/error.go | 11 ++++++++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 66b30c98e2a0a..38169784b7223 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -54,20 +54,20 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [ results, err := a.query.Eval(ctx, rego.EvalInput(input)) if err != nil { - return ForbiddenWithInternal(xerrors.Errorf("eval rego: %w, err"), input) + return ForbiddenWithInternal(xerrors.Errorf("eval rego: %w, err"), input, results) } if len(results) != 1 { - return ForbiddenWithInternal(xerrors.Errorf("expect only 1 result, got %d", len(results)), input) + return ForbiddenWithInternal(xerrors.Errorf("expect only 1 result, got %d", len(results)), input, results) } allowedResult, ok := (results[0].Bindings["allowed"]).(bool) if !ok { - return ForbiddenWithInternal(xerrors.Errorf("expected allowed to be a bool but got %T", allowedResult), input) + return ForbiddenWithInternal(xerrors.Errorf("expected allowed to be a bool but got %T", allowedResult), input, results) } - if allowedResult { - return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input) + if !allowedResult { + return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results) } return nil diff --git a/coderd/rbac/authz_test.go b/coderd/rbac/authz_test.go index 35b5118e6c397..8454fed33dc2e 100644 --- a/coderd/rbac/authz_test.go +++ b/coderd/rbac/authz_test.go @@ -606,6 +606,7 @@ func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTes d, _ := json.Marshal(uerr.Input()) t.Logf("input: %s", string(d)) t.Logf("internal error: %+v", uerr.Internal().Error()) + t.Logf("output: %+v", uerr.Output()) } require.NoError(t, err, "expected no error for testcase action %s", a) continue diff --git a/coderd/rbac/error.go b/coderd/rbac/error.go index 7d86697e41c3c..593ca4d0fc23a 100644 --- a/coderd/rbac/error.go +++ b/coderd/rbac/error.go @@ -1,5 +1,7 @@ package rbac +import "github.com/open-policy-agent/opa/rego" + const ( // errUnauthorized is the error message that should be returned to // clients when an action is forbidden. It is intentionally vague to prevent @@ -13,18 +15,20 @@ type UnauthorizedError struct { // It is only for debugging purposes. internal error input map[string]interface{} + output rego.ResultSet } // ForbiddenWithInternal creates a new error that will return a simple // "forbidden" to the client, logging internally the more detailed message // provided. -func ForbiddenWithInternal(internal error, input map[string]interface{}) *UnauthorizedError { +func ForbiddenWithInternal(internal error, input map[string]interface{}, output rego.ResultSet) *UnauthorizedError { if input == nil { input = map[string]interface{}{} } return &UnauthorizedError{ internal: internal, input: input, + output: output, } } @@ -41,3 +45,8 @@ func (e *UnauthorizedError) Internal() error { func (e *UnauthorizedError) Input() map[string]interface{} { return e.input } + +// Output contains the results of the Rego query for debugging. +func (e *UnauthorizedError) Output() rego.ResultSet { + return e.output +} From b22b723ed262a75c820bd2eb5676a5bde1e44d81 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 12 Apr 2022 09:17:58 -0500 Subject: [PATCH 71/75] to-done --- coderd/rbac/example_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/coderd/rbac/example_test.go b/coderd/rbac/example_test.go index 56b47e89f71fd..814397741630c 100644 --- a/coderd/rbac/example_test.go +++ b/coderd/rbac/example_test.go @@ -12,7 +12,6 @@ import ( // TestExample gives some examples on how to use the authz library. // This serves to test syntax more than functionality. func TestExample(t *testing.T) { - t.Skip("TODO: unskip when rego is done") t.Parallel() ctx := context.Background() authorizer, err := rbac.NewAuthorizer() @@ -28,14 +27,12 @@ func TestExample(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 := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ResourceWorkspace.All(), rbac.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 From c2b1dde21e482be82548800a61bfd6b7925f22b0 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 12 Apr 2022 09:30:11 -0500 Subject: [PATCH 72/75] Remove unused fields --- coderd/rbac/authz.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 38169784b7223..19561e327d058 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -36,10 +36,6 @@ func NewAuthorizer() (*RegoAuthorizer, error) { type authSubject struct { ID string `json:"id"` Roles []Role `json:"roles"` - - SitePermissions []Permission `json:"site_permissions"` - OrgPermissions []Permission `json:"org_permissions"` - UserPermissions []Permission `json:"user_permissions"` } func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, object Object, action Action) error { From cfdd2cb78f7ab0a459136b36f1a6dc138b923aa9 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 12 Apr 2022 09:48:34 -0500 Subject: [PATCH 73/75] Add some comments to rego --- coderd/rbac/policy.rego | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/coderd/rbac/policy.rego b/coderd/rbac/policy.rego index 6fe659296fed2..15d986ca08d32 100644 --- a/coderd/rbac/policy.rego +++ b/coderd/rbac/policy.rego @@ -19,7 +19,8 @@ bool_flip(b) = flipped { flipped = true } -# perms_grant returns a set of boolean values (true, false). +# perms_grant returns a set of boolean values {true, false}. +# True means a positive permission in the set, false is a negative permission. # It will only return `bool_flip(perm.negate)` for permissions that affect a given # resource_type, resource_id, and action. # The empty set is returned if no relevant permissions are found. @@ -41,7 +42,7 @@ perms_grant(permissions) = grants { default site = {} site = grant { # Boolean set for all site wide permissions. - grant = { v | # Use set comprehension to remove dulpicate values + grant = { v | # Use set comprehension to remove duplicate values # For each role, grab the site permission. # Find the grants on this permission list. v = perms_grant(input.subject.roles[_].site)[_] @@ -53,7 +54,7 @@ user = grant { # Only apply user permissions if the user owns the resource input.object.owner != "" input.object.owner == input.subject.id - grant = { v | # Use set comprehension to remove dulpicate values + grant = { v | # For each role, grab the user permissions. # Find the grants on this permission list. v = perms_grant(input.subject.roles[_].user)[_] @@ -84,12 +85,15 @@ org_non_member { } # org is two rules that equate to the following -# if !org_non_member { return org_member } -# else {false} +# if org_non_member { return {false} } +# else { org_member } # # It is important both rules cannot be true, as the `org` rules cannot produce multiple outputs. -default org = [] +default org = {} org = set { + # We have to do !org_non_member because rego rules must evaluate to 'true' + # to have a value set. + # So we do "not not-org-member" which means "subject is in org" not org_non_member set = org_member } @@ -112,6 +116,8 @@ allow { site[_] } +# OR + # org allow allow { # No site or org deny @@ -121,6 +127,8 @@ allow { org[_] } +# OR + # user allow allow { # No site, org, or user deny From 5c113a09ee994e5e6eef4625f938ec9c9ca330e2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 12 Apr 2022 09:58:46 -0500 Subject: [PATCH 74/75] Move Authorize param order --- coderd/rbac/authz.go | 2 +- coderd/rbac/authz_test.go | 2 +- coderd/rbac/example_test.go | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 19561e327d058..f1ae3335cc4d8 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -38,7 +38,7 @@ type authSubject struct { Roles []Role `json:"roles"` } -func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, object Object, action Action) error { +func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, action Action, object Object) error { input := map[string]interface{}{ "subject": authSubject{ ID: subjectID, diff --git a/coderd/rbac/authz_test.go b/coderd/rbac/authz_test.go index 8454fed33dc2e..c37b3d0f83a10 100644 --- a/coderd/rbac/authz_test.go +++ b/coderd/rbac/authz_test.go @@ -598,7 +598,7 @@ func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTes for _, c := range cases { t.Run(name, func(t *testing.T) { for _, a := range c.actions { - err := authorizer.Authorize(context.Background(), subject.UserID, subject.Roles, c.resource, a) + err := authorizer.Authorize(context.Background(), subject.UserID, subject.Roles, a, c.resource) if c.allow { if err != nil { var uerr *rbac.UnauthorizedError diff --git a/coderd/rbac/example_test.go b/coderd/rbac/example_test.go index 814397741630c..e9c4222f9c4a9 100644 --- a/coderd/rbac/example_test.go +++ b/coderd/rbac/example_test.go @@ -30,7 +30,7 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("ReadAllWorkspaces", func(t *testing.T) { // To read all workspaces on the site - err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ResourceWorkspace.All(), rbac.ActionRead) + err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.All()) var _ = err require.Error(t, err, "this user cannot read all workspaces") }) @@ -38,17 +38,17 @@ func TestExample(t *testing.T) { //nolint:paralleltest t.Run("ReadOrgWorkspaces", func(t *testing.T) { // To read all workspaces on the org 'default' - err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ResourceWorkspace.InOrg("default"), rbac.ActionRead) + err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg("default")) 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 := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ResourceWorkspace.InOrg("default").WithOwner(user.UserID), rbac.ActionRead) + err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg("default").WithOwner(user.UserID)) require.NoError(t, err, "this user can their workspace") - err = authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234"), rbac.ActionRead) + err = authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234")) require.NoError(t, err, "this user can read workspace '1234'") }) } From 44a7679ed8edb3579188f9c5c9f3b3d28552ea49 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 12 Apr 2022 10:04:30 -0500 Subject: [PATCH 75/75] Drop resources.go file --- coderd/rbac/object.go | 19 +++++++++++++++++++ coderd/rbac/resources.go | 18 ------------------ 2 files changed, 19 insertions(+), 18 deletions(-) delete mode 100644 coderd/rbac/resources.go diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index b82b05b11fc5d..c8b7c94f1d9dc 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -1,5 +1,24 @@ package rbac +const WildcardSymbol = "*" + +// Resources are just typed objects. Making resources this way allows directly +// passing them into an Authorize function and use the chaining api. +var ( + ResourceWorkspace = Object{ + Type: "workspace", + } + + ResourceTemplate = Object{ + Type: "template", + } + + // ResourceWildcard represents all resource types + ResourceWildcard = Object{ + Type: WildcardSymbol, + } +) + // Object 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 Object diff --git a/coderd/rbac/resources.go b/coderd/rbac/resources.go deleted file mode 100644 index b8f79fb947b35..0000000000000 --- a/coderd/rbac/resources.go +++ /dev/null @@ -1,18 +0,0 @@ -package rbac - -const WildcardSymbol = "*" - -var ( - ResourceWorkspace = Object{ - Type: "workspace", - } - - ResourceTemplate = Object{ - Type: "template", - } - - // ResourceWildcard represents all resource types - ResourceWildcard = Object{ - Type: WildcardSymbol, - } -)