Skip to content

Commit 4cf4808

Browse files
committed
checkpoint: crowd programming: define and simplify top-level API
1 parent 9918c16 commit 4cf4808

20 files changed

+69
-1415
lines changed

coderd/authz/authz.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package authz
22

3+
import "errors"
4+
5+
var ErrUnauthorized = errors.New("unauthorized")
6+
37
// TODO: Implement Authorize
48
func Authorize(subj Subject, obj Resource, action Action) error {
59
// TODO: Expand subject roles into their permissions as appropriate. Apply scopes.
@@ -8,5 +12,7 @@ func Authorize(subj Subject, obj Resource, action Action) error {
812

913
// AuthorizePermissions runs the authorize function with the raw permissions in a single list.
1014
func AuthorizePermissions(_ string, _ []Permission, _ Resource, _ Action) error {
11-
return nil
15+
// return nil
16+
// for now, nothing is allowed
17+
return ErrUnauthorized
1218
}

coderd/authz/authz_test.go

+32-233
Original file line numberDiff line numberDiff line change
@@ -1,258 +1,57 @@
11
package authz_test
22

33
import (
4-
"math/bits"
5-
"strings"
64
"testing"
75

6+
"github.com/stretchr/testify/require"
7+
88
"github.com/coder/coder/coderd/authz"
9-
"github.com/coder/coder/coderd/authz/authztest"
109
)
1110

12-
var nilSet = authztest.Set{nil}
13-
14-
func TestExhaustiveAuthorize(t *testing.T) {
11+
func TestAuthorize(t *testing.T) {
1512
t.Parallel()
1613

17-
all := authztest.GroupedPermissions(authztest.AllPermissions())
18-
roleVariants := permissionVariants(all)
19-
res := authz.ResourceType(authztest.PermObjectType).AsID(authztest.PermObjectID)
20-
2114
testCases := []struct {
22-
Name string
23-
Objs []authz.Resource
24-
// Action is constant
25-
// Subject comes from roleVariants
26-
Result func(pv string) bool
15+
name string
16+
subject authz.Subject
17+
resource authz.Resource
18+
actions []authz.Action
19+
error string
2720
}{
2821
{
29-
Name: "User:Org",
30-
Objs: []authz.Resource{
31-
res.Owner(authztest.PermMe).Org(authztest.PermOrgID),
32-
},
33-
Result: func(pv string) bool {
34-
return strings.Contains(pv, "+")
22+
name: "unauthenticated user cannot perform an action",
23+
subject: authz.SubjectTODO{
24+
UserID: "",
25+
Site: []authz.Role{authz.RoleNoPerm},
3526
},
27+
resource: authz.ResourceWorkspace,
28+
actions: []authz.Action{authz.ActionRead, authz.ActionCreate, authz.ActionDelete, authz.ActionUpdate},
29+
error: "unauthorized",
3630
},
3731
{
38-
// All U+/- tests should fail
39-
Name: "NotUser:Org",
40-
Objs: []authz.Resource{
41-
res.Owner("other").Org(authztest.PermOrgID),
42-
res.Owner("").Org(authztest.PermOrgID),
43-
},
44-
Result: func(pv string) bool {
45-
if strings.Contains(pv, "U") {
46-
return false
47-
}
48-
return strings.Contains(pv, "+")
32+
name: "admin can do anything",
33+
subject: authz.SubjectTODO{
34+
UserID: "admin",
35+
Site: []authz.Role{authz.RoleAllowAll},
4936
},
37+
resource: authz.ResourceWorkspace,
38+
actions: []authz.Action{authz.ActionRead, authz.ActionCreate, authz.ActionDelete, authz.ActionUpdate},
39+
error: "",
5040
},
51-
{
52-
// All O+/- and U+/- tests should fail
53-
Name: "NotUser:NotOrg",
54-
Objs: []authz.Resource{
55-
res.Owner(authztest.PermMe).Org("non-mem"),
56-
res.Owner("other").Org("non-mem"),
57-
res.Owner("other").Org(""),
58-
res.Owner("").Org("non-mem"),
59-
res.Owner("").Org(""),
60-
},
61-
62-
Result: func(pv string) bool {
63-
if strings.Contains(pv, "U") {
64-
return false
65-
}
66-
if strings.Contains(pv, "O") {
67-
return false
68-
}
69-
return strings.Contains(pv, "+")
70-
},
71-
},
72-
// TODO: @emyrk for this one, we should probably pass a custom roles variant
73-
//{
74-
// // O+, O- no longer pass judgement. Defer to user level judgement (only somewhat tricky case)
75-
// Name: "User:NotOrg",
76-
// Objs: authztest.Objects(
77-
// []string{authztest.PermMe, ""},
78-
// ),
79-
// Result: func(pv string) bool {
80-
// return strings.Contains(pv, "+")
81-
// },
82-
// },
8341
}
8442

85-
failedTests := make(map[string]int)
86-
//nolint:paralleltest
87-
for _, c := range testCases {
88-
t.Run(c.Name, func(t *testing.T) {
89-
for _, o := range c.Objs {
90-
for name, v := range roleVariants {
91-
v.Each(func(set authztest.Set) {
92-
// TODO: Authz.Permissions does allocations at the moment. We should fix that.
93-
err := authz.AuthorizePermissions(
94-
authztest.PermMe,
95-
set.Permissions(),
96-
o,
97-
authztest.PermAction)
98-
if c.Result(name) && err != nil {
99-
failedTests[name]++
100-
} else if !c.Result(name) && err == nil {
101-
failedTests[name]++
102-
}
103-
})
104-
v.Reset()
43+
for _, testCase := range testCases {
44+
testCase := testCase
45+
t.Run(testCase.name, func(t *testing.T) {
46+
t.Parallel()
47+
for _, action := range testCase.actions {
48+
err := authz.Authorize(testCase.subject, testCase.resource, action)
49+
if testCase.error == "" {
50+
require.NoError(t, err, "expected no error for testcase testcase %q action %s", testCase.name, action)
51+
continue
10552
}
53+
require.EqualError(t, err, testCase.error, "unexpected error")
10654
}
10755
})
10856
}
109-
// TODO: @emyrk when we implement the correct authorize, we can enable this check.
110-
// for testName, numFailed := range failedTests {
111-
// require.Zero(t, failedTests[testName], fmt.Sprintf("%s: %d tests failed", testName, numFailed))
112-
// }
113-
}
114-
115-
func permissionVariants(all authztest.SetGroup) map[string]*authztest.Role {
116-
// an is any noise above the impactful set
117-
an := noiseAbstain
118-
// ln is any noise below the impactful set
119-
ln := noisePositive | noiseNegative | noiseAbstain
120-
121-
// Cases are X+/- where X indicates the level where the impactful set is.
122-
// The impactful set determines the result.
123-
return map[string]*authztest.Role{
124-
// Wild
125-
"W+": authztest.NewRole(
126-
pos(all.Wildcard()),
127-
noise(ln, all.Site(), all.Org(), all.User()),
128-
),
129-
"W-": authztest.NewRole(
130-
neg(all.Wildcard()),
131-
noise(ln, all.Site(), all.Org(), all.User()),
132-
),
133-
// Site
134-
"S+": authztest.NewRole(
135-
noise(an, all.Wildcard()),
136-
pos(all.Site()),
137-
noise(ln, all.Org(), all.User()),
138-
),
139-
"S-": authztest.NewRole(
140-
noise(an, all.Wildcard()),
141-
neg(all.Site()),
142-
noise(ln, all.Org(), all.User()),
143-
),
144-
// Org:* -- Added org:mem noise
145-
"O+": authztest.NewRole(
146-
noise(an, all.Wildcard(), all.Site(), all.OrgMem()),
147-
pos(all.Org()),
148-
noise(ln, all.User()),
149-
),
150-
"O-": authztest.NewRole(
151-
noise(an, all.Wildcard(), all.Site(), all.OrgMem()),
152-
neg(all.Org()),
153-
noise(ln, all.User()),
154-
),
155-
// Org:Mem -- Added org:* noise
156-
"M+": authztest.NewRole(
157-
noise(an, all.Wildcard(), all.Site(), all.Org()),
158-
pos(all.OrgMem()),
159-
noise(ln, all.User()),
160-
),
161-
"M-": authztest.NewRole(
162-
noise(an, all.Wildcard(), all.Site(), all.Org()),
163-
neg(all.OrgMem()),
164-
noise(ln, all.User()),
165-
),
166-
// User
167-
"U+": authztest.NewRole(
168-
noise(an, all.Wildcard(), all.Site(), all.Org()),
169-
pos(all.User()),
170-
),
171-
"U-": authztest.NewRole(
172-
noise(an, all.Wildcard(), all.Site(), all.Org()),
173-
neg(all.User()),
174-
),
175-
// Abstain
176-
"A+": authztest.NewRole(
177-
authztest.Union(
178-
all.Wildcard().Abstain(),
179-
all.Site().Abstain(),
180-
all.Org().Abstain(),
181-
all.OrgMem().Abstain(),
182-
all.User().Abstain(),
183-
),
184-
all.User().Positive()[:1],
185-
),
186-
"A-": authztest.NewRole(
187-
authztest.Union(
188-
all.Wildcard().Abstain(),
189-
all.Site().Abstain(),
190-
all.Org().Abstain(),
191-
all.OrgMem().Abstain(),
192-
all.User().Abstain(),
193-
),
194-
),
195-
}
196-
}
197-
198-
// pos returns the positive impactful variant for a given level. It does not
199-
// include noise at any other level but the one given.
200-
func pos(lvl authztest.LevelGroup) *authztest.Role {
201-
return authztest.NewRole(
202-
lvl.Positive(),
203-
authztest.Union(lvl.Abstain()[:1], nilSet),
204-
)
205-
}
206-
207-
func neg(lvl authztest.LevelGroup) *authztest.Role {
208-
return authztest.NewRole(
209-
lvl.Negative(),
210-
authztest.Union(lvl.Positive()[:1], nilSet),
211-
authztest.Union(lvl.Abstain()[:1], nilSet),
212-
)
213-
}
214-
215-
type noiseBits uint8
216-
217-
const (
218-
_ noiseBits = 1 << iota
219-
noisePositive
220-
noiseNegative
221-
noiseAbstain
222-
)
223-
224-
func flagMatch(flag, in noiseBits) bool {
225-
return flag&in != 0
226-
}
227-
228-
// noise returns the noise permission permutations for a given level. You can
229-
// use this helper function when this level is not impactful.
230-
// The returned role is the permutations including at least one example of
231-
// positive, negative, and neutral permissions. It also includes the set of
232-
// no additional permissions.
233-
func noise(f noiseBits, lvls ...authztest.LevelGroup) *authztest.Role {
234-
rs := make([]authztest.Iterable, 0, len(lvls))
235-
for _, lvl := range lvls {
236-
sets := make([]authztest.Iterable, 0, bits.OnesCount8(uint8(f)))
237-
238-
if flagMatch(noisePositive, f) {
239-
sets = append(sets, authztest.Union(lvl.Positive()[:1], nilSet))
240-
}
241-
if flagMatch(noiseNegative, f) {
242-
sets = append(sets, authztest.Union(lvl.Negative()[:1], nilSet))
243-
}
244-
if flagMatch(noiseAbstain, f) {
245-
sets = append(sets, authztest.Union(lvl.Abstain()[:1], nilSet))
246-
}
247-
248-
rs = append(rs, authztest.NewRole(
249-
sets...,
250-
))
251-
}
252-
253-
if len(rs) == 1 {
254-
role, _ := rs[0].(*authztest.Role)
255-
return role
256-
}
257-
return authztest.NewRole(rs...)
25857
}

0 commit comments

Comments
 (0)