Skip to content

Commit 2804b92

Browse files
committed
test: ParsePermissions from strings
Add some examples
1 parent 913d141 commit 2804b92

File tree

8 files changed

+240
-11
lines changed

8 files changed

+240
-11
lines changed

coderd/authz/action.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ type Action string
55

66
const (
77
ActionRead = "read"
8-
ActionWrite = "write"
8+
ActionCreate = "create"
99
ActionModify = "modify"
1010
ActionDelete = "delete"
1111
)

coderd/authz/authztest/iterator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func RandomSet(size int) authztest.Set {
5353
func RandomPermission() authz.Permission {
5454
actions := []authz.Action{
5555
authz.ActionRead,
56-
authz.ActionWrite,
56+
authz.ActionCreate,
5757
authz.ActionModify,
5858
authz.ActionDelete,
5959
}

coderd/authz/example_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package authz_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/coder/coder/coderd/authz"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// Test_Example gives some examples on how to use the authz library.
11+
// This serves to test syntax more than functionality.
12+
func Test_Example(t *testing.T) {
13+
t.Parallel()
14+
15+
// user will become an authn object, and can even be a database.User if it
16+
// fulfills the interface. Until then, use a placeholder.
17+
user := authz.SubjectTODO{
18+
UserID: "alice",
19+
// No site perms
20+
Site: []authz.Role{},
21+
Org: map[string][]authz.Role{
22+
// Admin of org "default".
23+
"default": {{Permissions: must(authz.ParsePermissions("+org.*.*.*"))}},
24+
},
25+
User: []authz.Role{
26+
// Site user role
27+
{Permissions: must(authz.ParsePermissions("+user.*.*.*"))},
28+
},
29+
}
30+
31+
// TODO: Uncomment all assertions when implementation is done.
32+
33+
t.Run("ReadAllWorkspaces", func(t *testing.T) {
34+
// To read all workspaces on the site
35+
err := authz.Authorize(user, authz.ResourceWorkspace, authz.ActionRead)
36+
var _ = err
37+
//require.Error(t, err, "this user cannot read all workspaces")
38+
})
39+
40+
t.Run("ReadOrgWorkspaces", func(t *testing.T) {
41+
// To read all workspaces on the org 'default'
42+
err := authz.Authorize(user, authz.ResourceWorkspace.Org("default"), authz.ActionRead)
43+
require.NoError(t, err, "this user can read all org workspaces in 'default'")
44+
})
45+
46+
t.Run("ReadMyWorkspace", func(t *testing.T) {
47+
// Note 'database.Workspace' could fulfill the object interface and be passed in directly
48+
err := authz.Authorize(user, authz.ResourceWorkspace.Org("default").Owner(user.UserID), authz.ActionRead)
49+
require.NoError(t, err, "this user can their workspace")
50+
51+
err = authz.Authorize(user, authz.ResourceWorkspace.Org("default").Owner(user.UserID).AsID("1234"), authz.ActionRead)
52+
require.NoError(t, err, "this user can read workspace '1234'")
53+
})
54+
55+
t.Run("CreateNewSiteUser", func(t *testing.T) {
56+
err := authz.Authorize(user, authz.ResourceUser, authz.ActionCreate)
57+
var _ = err
58+
//require.Error(t, err, "this user cannot create new users")
59+
})
60+
}
61+
62+
func must[r any](v r, err error) r {
63+
if err != nil {
64+
panic(err)
65+
}
66+
return v
67+
}

coderd/authz/permission.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package authz
22

3-
import "strings"
3+
import (
4+
"golang.org/x/xerrors"
5+
"strings"
6+
)
47

58
type permLevel string
69

@@ -51,3 +54,67 @@ func (p Permission) String() string {
5154
s.WriteString(string(p.Action))
5255
return s.String()
5356
}
57+
58+
func ParsePermissions(perms string) ([]Permission, error) {
59+
permList := strings.Split(perms, ",")
60+
parsed := make([]Permission, 0, len(permList))
61+
for _, permStr := range permList {
62+
p, err := ParsePermission(strings.TrimSpace(permStr))
63+
if err != nil {
64+
return nil, xerrors.Errorf("perm '%s': %w", permStr, err)
65+
}
66+
parsed = append(parsed, p)
67+
}
68+
return parsed, nil
69+
}
70+
71+
func ParsePermission(perm string) (Permission, error) {
72+
parts := strings.Split(perm, ".")
73+
if len(parts) != 4 {
74+
return Permission{}, xerrors.Errorf("permission expects 4 parts, got %d", len(parts))
75+
}
76+
77+
level, resType, resID, act := parts[0], parts[1], parts[2], parts[3]
78+
79+
if len(level) < 2 {
80+
return Permission{}, xerrors.Errorf("permission level is too short: '%s'", parts[0])
81+
}
82+
sign := level[0]
83+
levelParts := strings.Split(level[1:], ":")
84+
if len(levelParts) > 2 {
85+
return Permission{}, xerrors.Errorf("unsupported level format")
86+
}
87+
88+
var permission Permission
89+
90+
switch sign {
91+
case '+':
92+
permission.Sign = true
93+
case '-':
94+
default:
95+
return Permission{}, xerrors.Errorf("sign must be +/-")
96+
}
97+
98+
switch permLevel(strings.ToLower(levelParts[0])) {
99+
case LevelWildcard:
100+
permission.Level = LevelWildcard
101+
case LevelSite:
102+
permission.Level = LevelSite
103+
case LevelOrg:
104+
permission.Level = LevelOrg
105+
case LevelUser:
106+
permission.Level = LevelUser
107+
default:
108+
return Permission{}, xerrors.Errorf("'%s' is an unsupported level", levelParts[0])
109+
}
110+
111+
if len(levelParts) > 1 {
112+
permission.LevelID = levelParts[1]
113+
}
114+
115+
// might want to check if these are valid resource types and actions.
116+
permission.ResourceType = ResourceType(resType)
117+
permission.ResourceID = resID
118+
permission.Action = Action(act)
119+
return permission, nil
120+
}

coderd/authz/permission_test.go

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ func Test_PermissionString(t *testing.T) {
3535
LevelID: "",
3636
ResourceType: authz.ResourceDevURL,
3737
ResourceID: "1234",
38-
Action: authz.ActionWrite,
38+
Action: authz.ActionCreate,
3939
},
40-
Expected: "-user.devurl.1234.write",
40+
Expected: "-user.devurl.1234.create",
4141
},
4242
{
4343
Name: "OrgID",
@@ -56,7 +56,85 @@ func Test_PermissionString(t *testing.T) {
5656
for _, c := range testCases {
5757
t.Run(c.Name, func(t *testing.T) {
5858
require.Equal(t, c.Expected, c.Permission.String())
59+
perm, err := authz.ParsePermission(c.Expected)
60+
require.NoError(t, err, "parse perm string")
61+
require.Equal(t, c.Permission, perm, "expected perm")
62+
63+
perms, err := authz.ParsePermissions(c.Expected)
64+
require.NoError(t, err, "parse perms string")
65+
require.Equal(t, c.Permission, perms[0], "expected perm")
66+
require.Len(t, perms, 1, "expect 1 perm")
5967
})
6068
}
69+
}
70+
71+
func Test_ParsePermissions(t *testing.T) {
72+
t.Parallel()
6173

74+
testCases := []struct {
75+
Name string
76+
Str string
77+
Permissions []authz.Permission
78+
ErrStr string
79+
}{
80+
{
81+
Name: "NoSign",
82+
Str: "site.*.*.*",
83+
ErrStr: "sign must be +/-",
84+
},
85+
{
86+
Name: "BadLevel",
87+
Str: "+unknown.*.*.*",
88+
ErrStr: "unsupported level",
89+
},
90+
{
91+
Name: "NotEnoughParts",
92+
Str: "+*.*.*",
93+
ErrStr: "permission expects 4 parts",
94+
},
95+
{
96+
Name: "ShortLevel",
97+
Str: "*.*.*.*",
98+
ErrStr: "permission level is too short",
99+
},
100+
{
101+
Name: "BadLevelID",
102+
Str: "org:1234:extra.*.*.*",
103+
ErrStr: "unsupported level format",
104+
},
105+
{
106+
Name: "GoodSet",
107+
Str: "+org:1234.workspace.5678.read, -site.*.*.create",
108+
Permissions: []authz.Permission{
109+
{
110+
Sign: true,
111+
Level: "org",
112+
LevelID: "1234",
113+
ResourceType: authz.ResourceWorkspace,
114+
ResourceID: "5678",
115+
Action: authz.ActionRead,
116+
},
117+
{
118+
Sign: false,
119+
Level: "site",
120+
LevelID: "",
121+
ResourceType: "*",
122+
ResourceID: "*",
123+
Action: authz.ActionCreate,
124+
},
125+
},
126+
},
127+
}
128+
for _, c := range testCases {
129+
t.Run(c.Name, func(t *testing.T) {
130+
perms, err := authz.ParsePermissions(c.Str)
131+
if c.ErrStr != "" {
132+
require.Error(t, err)
133+
require.Contains(t, err.Error(), c.ErrStr, "exp error")
134+
} else {
135+
require.NoError(t, err, "parse error")
136+
require.Equal(t, c.Permissions, perms, "exp perms")
137+
}
138+
})
139+
}
62140
}

coderd/authz/resources.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const (
77
ResourceWorkspace ResourceType = "workspace"
88
ResourceProject ResourceType = "project"
99
ResourceDevURL ResourceType = "devurl"
10+
ResourceUser ResourceType = "user"
1011
)
1112

1213
func (t ResourceType) ID() string {

coderd/authz/role.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package authz
22

33
type Role struct {
4-
Level permLevel
54
Permissions []Permission
65
}

coderd/authz/subject.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ type Subject interface {
2626
type SubjectTODO struct {
2727
UserID string `json:"user_id"`
2828

29-
Site []Role `json:"site_roles"`
30-
Org []Role `json:"org_roles"`
31-
User []Role `json:"user_roles"`
29+
Site []Role `json:"site_roles"`
30+
Org map[string][]Role `json:"org_roles"`
31+
User []Role `json:"user_roles"`
3232
}
3333

3434
func (s SubjectTODO) ID() string {
@@ -39,8 +39,25 @@ func (s SubjectTODO) SiteRoles() ([]Role, error) {
3939
return s.Site, nil
4040
}
4141

42-
func (s SubjectTODO) OrgRoles() ([]Role, error) {
43-
return s.Org, nil
42+
func (s SubjectTODO) OrgRoles(_ context.Context, orgID string) ([]Role, error) {
43+
v, ok := s.Org[orgID]
44+
if !ok {
45+
// Members not in an org return the negative perm
46+
return []Role{{
47+
Permissions: []Permission{
48+
{
49+
Sign: false,
50+
Level: "*",
51+
LevelID: "",
52+
ResourceType: "*",
53+
ResourceID: "*",
54+
Action: "*",
55+
},
56+
},
57+
}}, nil
58+
}
59+
60+
return v, nil
4461
}
4562

4663
func (s SubjectTODO) UserRoles() ([]Role, error) {

0 commit comments

Comments
 (0)