Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

chore: authz 'any_org' to return if at least 1 org has perms#14009

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Emyrk merged 8 commits intomainfromstevenmasley/authcheck_any_org
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletionscoderd/apidoc/docs.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

4 changes: 4 additions & 0 deletionscoderd/apidoc/swagger.json
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

7 changes: 4 additions & 3 deletionscoderd/authorize.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,9 +167,10 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
}

obj := rbac.Object{
Owner: v.Object.OwnerID,
OrgID: v.Object.OrganizationID,
Type: string(v.Object.ResourceType),
Owner: v.Object.OwnerID,
OrgID: v.Object.OrganizationID,
Type: string(v.Object.ResourceType),
AnyOrgOwner: v.Object.AnyOrgOwner,
}
if obj.Owner == "me" {
obj.Owner = auth.ID
Expand Down
4 changes: 4 additions & 0 deletionscoderd/rbac/astvalue.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,10 @@ func (z Object) regoValue() ast.Value {
ast.StringTerm("org_owner"),
ast.StringTerm(z.OrgID),
},
[2]*ast.Term{
ast.StringTerm("any_org"),
ast.BooleanTerm(z.AnyOrgOwner),
},
[2]*ast.Term{
ast.StringTerm("type"),
ast.StringTerm(z.Type),
Expand Down
9 changes: 8 additions & 1 deletioncoderd/rbac/authz.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -181,7 +181,7 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subject Subject, a
for _, o := range objects {
rbacObj := o.RBACObject()
if rbacObj.Type != objectType {
return nil, xerrors.Errorf("object types must be uniform across the set (%s), found %s", objectType, rbacObj)
return nil, xerrors.Errorf("object types must be uniform across the set (%s), found %s", objectType, rbacObj.Type)
}
err := auth.Authorize(ctx, subject, action, o.RBACObject())
if err == nil {
Expand DownExpand Up@@ -387,6 +387,13 @@ func (a RegoAuthorizer) authorize(ctx context.Context, subject Subject, action p
return xerrors.Errorf("subject must have a scope")
}

// The caller should use either 1 or the other (or none).
// Using "AnyOrgOwner" and an OrgID is a contradiction.
// An empty uuid or a nil uuid means "no org owner".
if object.AnyOrgOwner && !(object.OrgID == "" || object.OrgID == "00000000-0000-0000-0000-000000000000") {
return xerrors.Errorf("object cannot have 'any_org' and an 'org_id' specified, values are mutually exclusive")
}

astV, err := regoInputValue(subject, action, object)
if err != nil {
return xerrors.Errorf("convert input to value: %w", err)
Expand Down
45 changes: 38 additions & 7 deletionscoderd/rbac/authz_internal_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -291,6 +291,22 @@ func TestAuthorizeDomain(t *testing.T) {
unuseID := uuid.New()
allUsersGroup := "Everyone"

// orphanedUser has no organization
orphanedUser := Subject{
ID: "me",
Scope: must(ExpandScope(ScopeAll)),
Groups: []string{},
Roles: Roles{
must(RoleByName(RoleMember())),
},
}
testAuthorize(t, "OrphanedUser", orphanedUser, []authTestCase{
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(orphanedUser.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},

// Orphaned user cannot create workspaces in any organization
{resource: ResourceWorkspace.AnyOrganization().WithOwner(orphanedUser.ID), actions: []policy.Action{policy.ActionCreate}, allow: false},
})

user := Subject{
ID: "me",
Scope: must(ExpandScope(ScopeAll)),
Expand DownExpand Up@@ -370,6 +386,10 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: false},

// AnyOrganization using a user scoped permission
{resource: ResourceWorkspace.AnyOrganization().WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceTemplate.AnyOrganization(), actions: []policy.Action{policy.ActionCreate}, allow: false},

{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},

{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false},
Expand DownExpand Up@@ -443,6 +463,8 @@ func TestAuthorizeDomain(t *testing.T) {
workspaceExceptConnect := slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH)
workspaceConnect := []policy.Action{policy.ActionApplicationConnect, policy.ActionSSH}
testAuthorize(t, "OrgAdmin", user, []authTestCase{
{resource: ResourceTemplate.AnyOrganization(), actions: []policy.Action{policy.ActionCreate}, allow: true},

// Org + me
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceWorkspace.InOrg(defOrg), actions: workspaceExceptConnect, allow: true},
Expand DownExpand Up@@ -479,6 +501,9 @@ func TestAuthorizeDomain(t *testing.T) {
}

testAuthorize(t, "SiteAdmin", user, []authTestCase{
// Similar to an orphaned user, but has site level perms
{resource: ResourceTemplate.AnyOrganization(), actions: []policy.Action{policy.ActionCreate}, allow: true},

// Org + me
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceWorkspace.InOrg(defOrg), actions: ResourceWorkspace.AvailableActions(), allow: true},
Expand DownExpand Up@@ -1078,9 +1103,10 @@ func testAuthorize(t *testing.T, name string, subject Subject, sets ...[]authTes
t.Logf("input: %s", string(d))
if authError != nil {
var uerr *UnauthorizedError
xerrors.As(authError, &uerr)
t.Logf("internal error: %+v", uerr.Internal().Error())
t.Logf("output: %+v", uerr.Output())
if xerrors.As(authError, &uerr) {
t.Logf("internal error: %+v", uerr.Internal().Error())
t.Logf("output: %+v", uerr.Output())
}
}

if c.allow {
Expand DownExpand Up@@ -1115,10 +1141,15 @@ func testAuthorize(t *testing.T, name string, subject Subject, sets ...[]authTes
require.Equal(t, 0, len(partialAuthz.partialQueries.Support), "expected 0 support rules in scope authorizer")

partialErr := partialAuthz.Authorize(ctx, c.resource)
if authError != nil {
assert.Error(t, partialErr, "partial allowed invalid request (false positive)")
} else {
assert.NoError(t, partialErr, "partial error blocked valid request (false negative)")
// If 'AnyOrgOwner' is true, a partial eval does not make sense.
// Run the partial eval to ensure no panics, but the actual authz
// response does not matter.
if !c.resource.AnyOrgOwner {
if authError != nil {
assert.Error(t, partialErr, "partial allowed invalid request (false positive)")
} else {
assert.NoError(t, partialErr, "partial error blocked valid request (false negative)")
}
}
}
})
Expand Down
2 changes: 1 addition & 1 deletioncoderd/rbac/authz_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -314,7 +314,7 @@ func BenchmarkCacher(b *testing.B) {
}
}

funcTestCacher(t *testing.T) {
funcTestCache(t *testing.T) {
t.Parallel()

t.Run("NoCache", func(t *testing.T) {
Expand Down
27 changes: 27 additions & 0 deletionscoderd/rbac/object.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,12 @@ type Object struct {
Owner string `json:"owner"`
// OrgID specifies which org the object is a part of.
OrgID string `json:"org_owner"`
// AnyOrgOwner will disregard the org_owner when checking for permissions
// Use this to ask, "Can the actor do this action on any org?" when
// the exact organization is not important or known.
// E.g: The UI should show a "create template" button if the user
// can create a template in any org.
AnyOrgOwner bool `json:"any_org"`

// Type is "workspace", "project", "app", etc
Type string `json:"type"`
Expand DownExpand Up@@ -115,6 +121,7 @@ func (z Object) All() Object {
Type: z.Type,
ACLUserList: map[string][]policy.Action{},
ACLGroupList: map[string][]policy.Action{},
AnyOrgOwner: z.AnyOrgOwner,
}
}

Expand All@@ -126,6 +133,7 @@ func (z Object) WithIDString(id string) Object {
Type: z.Type,
ACLUserList: z.ACLUserList,
ACLGroupList: z.ACLGroupList,
AnyOrgOwner: z.AnyOrgOwner,
}
}

Expand All@@ -137,6 +145,7 @@ func (z Object) WithID(id uuid.UUID) Object {
Type: z.Type,
ACLUserList: z.ACLUserList,
ACLGroupList: z.ACLGroupList,
AnyOrgOwner: z.AnyOrgOwner,
}
}

Expand All@@ -149,6 +158,21 @@ func (z Object) InOrg(orgID uuid.UUID) Object {
Type: z.Type,
ACLUserList: z.ACLUserList,
ACLGroupList: z.ACLGroupList,
// InOrg implies AnyOrgOwner is false
AnyOrgOwner: false,
}
}

func (z Object) AnyOrganization() Object {
return Object{
ID: z.ID,
Owner: z.Owner,
// AnyOrgOwner cannot have an org owner also set.
OrgID: "",
Type: z.Type,
ACLUserList: z.ACLUserList,
ACLGroupList: z.ACLGroupList,
AnyOrgOwner: true,
}
}

Expand All@@ -161,6 +185,7 @@ func (z Object) WithOwner(ownerID string) Object {
Type: z.Type,
ACLUserList: z.ACLUserList,
ACLGroupList: z.ACLGroupList,
AnyOrgOwner: z.AnyOrgOwner,
}
}

Expand All@@ -173,6 +198,7 @@ func (z Object) WithACLUserList(acl map[string][]policy.Action) Object {
Type: z.Type,
ACLUserList: acl,
ACLGroupList: z.ACLGroupList,
AnyOrgOwner: z.AnyOrgOwner,
}
}

Expand All@@ -184,5 +210,6 @@ func (z Object) WithGroupACL(groups map[string][]policy.Action) Object {
Type: z.Type,
ACLUserList: z.ACLUserList,
ACLGroupList: groups,
AnyOrgOwner: z.AnyOrgOwner,
}
}
57 changes: 55 additions & 2 deletionscoderd/rbac/policy.rego
View file
Open in desktop
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I checked the RBAC benchmarks before/after and didn't see any concerning differences here 👍

Emyrk reacted with thumbs up emoji
Original file line numberDiff line numberDiff line change
Expand Up@@ -92,8 +92,18 @@ org := org_allow(input.subject.roles)
default scope_org := 0
scope_org := org_allow([input.scope])

org_allow(roles) := num {
allow := { id: num |
# org_allow_set is a helper function that iterates over all orgs that the actor
# is a member of. For each organization it sets the numerical allow value
# for the given object + action if the object is in the organization.
# The resulting value is a map that looks something like:
# {"10d03e62-7703-4df5-a358-4f76577d4e2f": 1, "5750d635-82e0-4681-bd44-815b18669d65": 1}
# The caller can use this output[<object.org_owner>] to get the final allow value.
#
# The reason we calculate this for all orgs, and not just the input.object.org_owner
# is that sometimes the input.object.org_owner is unknown. In those cases
# we have a list of org_ids that can we use in a SQL 'WHERE' clause.
org_allow_set(roles) := allow_set {
allow_set := { id: num |
id := org_members[_]
set := { x |
perm := roles[_].org[id][_]
Expand All@@ -103,6 +113,13 @@ org_allow(roles) := num {
}
num := number(set)
}
}

org_allow(roles) := num {
# If the object has "any_org" set to true, then use the other
# org_allow block.
not input.object.any_org
allow := org_allow_set(roles)

# Return only the org value of the input's org.
# The reason why we do not do this up front, is that we need to make sure
Expand All@@ -112,12 +129,47 @@ org_allow(roles) := num {
num := allow[input.object.org_owner]
}

# This block states if "object.any_org" is set to true, then disregard the
# organization id the object is associated with. Instead, we check if the user
# can do the action on any organization.
# This is useful for UI elements when we want to conclude, "Can the user create
# a new template in any organization?"
# It is easier than iterating over every organization the user is apart of.
org_allow(roles) := num {
input.object.any_org # if this is false, this code block is not used
allow := org_allow_set(roles)


# allow is a map of {"<org_id>": <number>}. We only care about values
# that are 1, and ignore the rest.
num := number([
keep |
# for every value in the mapping
value := allow[_]
# only keep values > 0.
# 1 = allow, 0 = abstain, -1 = deny
# We only need 1 explicit allow to allow the action.
# deny's and abstains are intentionally ignored.
value > 0
# result set is a set of [true,false,...]
# which "number()" will convert to a number.
keep := true
])
}

# 'org_mem' is set to true if the user is an org member
# If 'any_org' is set to true, use the other block to determine org membership.
org_mem := true {
not input.object.any_org
input.object.org_owner != ""
input.object.org_owner in org_members
}

org_mem := true {
input.object.any_org
count(org_members) > 0
}

org_ok {
org_mem
}
Expand All@@ -126,6 +178,7 @@ org_ok {
# the non-existent org.
org_ok {
input.object.org_owner == ""
not input.object.any_org
}

# User is the same as the site, except it only applies if the user owns the object and
Expand Down
40 changes: 40 additions & 0 deletionscoderd/rbac/roles_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -590,6 +590,46 @@ func TestRolePermissions(t *testing.T) {
false: {},
},
},
// AnyOrganization tests
{
Name: "CreateOrgMember",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceOrganizationMember.AnyOrganization(),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, userAdmin, orgAdmin, otherOrgAdmin, orgUserAdmin, otherOrgUserAdmin},
false: {
memberMe, templateAdmin,
orgTemplateAdmin, orgMemberMe, orgAuditor,
otherOrgMember, otherOrgAuditor, otherOrgTemplateAdmin,
},
},
},
{
Name: "CreateTemplateAnyOrg",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceTemplate.AnyOrganization(),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, orgAdmin, otherOrgAdmin},
false: {
userAdmin, memberMe,
orgMemberMe, orgAuditor, orgUserAdmin,
otherOrgMember, otherOrgAuditor, otherOrgUserAdmin,
},
},
},
Comment on lines +607 to +619
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Can you extend these tests to other organization-scoped RBAC objects?
I tried this out for workspaces and ran into behaviour I didn't expect:

{Name:     "CreateWorkspaceAnyOrg",Actions:  []policy.Action{policy.ActionCreate},Resource: rbac.ResourceWorkspace.AnyOrganization(),AuthorizeMap: map[bool][]hasAuthSubjects{true: {owner, orgAdmin, otherOrgAdmin, orgMemberMe},false: {memberMe, userAdmin, templateAdmin,orgAuditor, orgUserAdmin, orgTemplateAdmin,otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,},},},
    --- FAIL: TestRolePermissions/CreateWorkspaceAnyOrg (0.01s)        roles_test.go:675:                 Error Trace:    /workspaces/coder/coderd/rbac/roles_test.go:675                Error:          Received unexpected error:                                rbac: forbidden: (subject: { f8981f20-e4cc-4be9-a14c-c391200d086e [member organization-member:2c498c97-8c96-4742-9e37-3cd431780fd3] [] all <nil>}), (action: create), (object: {   true workspace map[] map[]}), (output: [])                Test:           TestRolePermissions/CreateWorkspaceAnyOrg                Messages:       Should pass: CreateWorkspaceAnyOrg as "org_member_me" doing "create" on "workspace"

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I can add more tests for sure. The test you wrote is missing withWithOwner. The test as is, asks the question "Can I create a workspace in any organization that is not owned by me?"

The correct question to see if you can make a workspace belonging to yourself is:

rbac.ResourceWorkspace.AnyOrganization().WithOwner(currentUser.String())

The reason the test without the owner works for some roles, is because owners and org admins are able to create workspaces on behalf of other users

Copy link
Member

@johnstcnjohnstcnJul 26, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Ah whoops 👍 I'm still holding the package wrong!

Emyrk reacted with thumbs up emoji
Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

It's not perfect for sure. Maybe there is some api wrapper that could exist that translates everything into some more readable language 🤷‍♂️.

I agree it's nuanced

{
Name: "CreateWorkspaceAnyOrg",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceWorkspace.AnyOrganization().WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, otherOrgAdmin, orgMemberMe},
false: {
memberMe, userAdmin, templateAdmin,
orgAuditor, orgUserAdmin, orgTemplateAdmin,
otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
},
},
},
}

// We expect every permission to be tested above.
Expand Down
3 changes: 3 additions & 0 deletionscodersdk/authorization.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,6 +54,9 @@ type AuthorizationObject struct {
// are using this option, you should also set the owner ID and organization ID
// if possible. Be as specific as possible using all the fields relevant.
ResourceID string `json:"resource_id,omitempty"`
// AnyOrgOwner (optional) will disregard the org_owner when checking for permissions.
// This cannot be set to true if the OrganizationID is set.
AnyOrgOwner bool `json:"any_org,omitempty"`
}

// AuthCheck allows the authenticated user to check if they have the given permissions
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp