mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: scope allow_list to include resource_type (#19748)
This feature allows the `allow_list` in the scopes to specify the `type`
This commit is contained in:
@@ -169,7 +169,7 @@ This command answers the question: “Is the user allowed?”
|
||||
### Partial Evaluation
|
||||
|
||||
```bash
|
||||
opa eval --partial --format=pretty 'data.authz.allow' -d policy.rego --unknowns input.object.owner --unknowns input.object.org_owner --unknowns input.object.acl_user_list --unknowns input.object.acl_group_list -i input.json
|
||||
opa eval --partial --format=pretty 'data.authz.allow' -d policy.rego --unknowns input.object.id --unknowns input.object.owner --unknowns input.object.org_owner --unknowns input.object.acl_user_list --unknowns input.object.acl_group_list -i input.json
|
||||
```
|
||||
|
||||
This command performs a partial evaluation of the policy, specifying a set of unknown input parameters.
|
||||
|
||||
+17
-1
@@ -182,9 +182,25 @@ func (s Scope) regoValue() ast.Value {
|
||||
if !ok {
|
||||
panic("developer error: role is not an object")
|
||||
}
|
||||
|
||||
terms := make([]*ast.Term, len(s.AllowIDList))
|
||||
for i, v := range s.AllowIDList {
|
||||
terms[i] = ast.NewTerm(ast.NewObject(
|
||||
[2]*ast.Term{
|
||||
ast.StringTerm("type"),
|
||||
ast.StringTerm(v.Type),
|
||||
},
|
||||
[2]*ast.Term{
|
||||
ast.StringTerm("id"),
|
||||
ast.StringTerm(v.ID),
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
r.Insert(
|
||||
ast.StringTerm("allow_list"),
|
||||
ast.NewTerm(regoSliceString(s.AllowIDList...)),
|
||||
ast.NewTerm(ast.NewArray(terms...)),
|
||||
)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -929,7 +929,7 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
Org: map[string][]Permission{},
|
||||
User: []Permission{},
|
||||
},
|
||||
AllowIDList: []string{workspaceID.String()},
|
||||
AllowIDList: []AllowListElement{{Type: ResourceWorkspace.Type, ID: workspaceID.String()}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1019,7 +1019,9 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
User: []Permission{},
|
||||
},
|
||||
// Empty string allow_list is allowed for actions like 'create'
|
||||
AllowIDList: []string{""},
|
||||
AllowIDList: []AllowListElement{{
|
||||
Type: ResourceWorkspace.Type, ID: "",
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1145,7 +1147,7 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
ResourceUser.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
AllowIDList: []string{policy.WildcardSymbol},
|
||||
AllowIDList: []AllowListElement{AllowListAll()},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1163,6 +1165,131 @@ func TestAuthorizeScope(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
func TestScopeAllowList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
defOrg := uuid.New()
|
||||
|
||||
// Some IDs to use
|
||||
wid := uuid.New()
|
||||
gid := uuid.New()
|
||||
|
||||
user := Subject{
|
||||
ID: "me",
|
||||
Roles: Roles{
|
||||
must(RoleByName(RoleOwner())),
|
||||
},
|
||||
Scope: Scope{
|
||||
Role: Role{
|
||||
Identifier: RoleIdentifier{
|
||||
Name: "AllowList",
|
||||
OrganizationID: defOrg,
|
||||
},
|
||||
DisplayName: "AllowList",
|
||||
// Allow almost everything
|
||||
Site: allPermsExcept(ResourceUser),
|
||||
},
|
||||
AllowIDList: []AllowListElement{
|
||||
{Type: ResourceWorkspace.Type, ID: wid.String()},
|
||||
{Type: ResourceWorkspace.Type, ID: ""}, // Allow to create
|
||||
{Type: ResourceTemplate.Type, ID: policy.WildcardSymbol},
|
||||
{Type: ResourceGroup.Type, ID: gid.String()},
|
||||
|
||||
// This scope allows all users, but the permissions do not.
|
||||
{Type: ResourceUser.Type, ID: policy.WildcardSymbol},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testAuthorize(t, "AllowList", user,
|
||||
// Allowed:
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.allow = true
|
||||
return c
|
||||
},
|
||||
[]authTestCase{
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID).WithID(wid), actions: []policy.Action{policy.ActionRead}},
|
||||
// matching on empty id
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionCreate}},
|
||||
|
||||
// Template has wildcard ID, so any uuid is allowed, including the empty
|
||||
{resource: ResourceTemplate.InOrg(defOrg).WithID(uuid.New()), actions: AllActions()},
|
||||
{resource: ResourceTemplate.InOrg(defOrg).WithID(uuid.New()), actions: AllActions()},
|
||||
{resource: ResourceTemplate.InOrg(defOrg), actions: AllActions()},
|
||||
|
||||
// Group
|
||||
{resource: ResourceGroup.InOrg(defOrg).WithID(gid), actions: []policy.Action{policy.ActionRead}},
|
||||
},
|
||||
),
|
||||
|
||||
// Not allowed:
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.allow = false
|
||||
return c
|
||||
},
|
||||
[]authTestCase{
|
||||
// Has the scope and allow list, but not the permission
|
||||
{resource: ResourceUser.WithOwner(user.ID), actions: []policy.Action{policy.ActionRead}},
|
||||
|
||||
// `wid` matches on the uuid, but not the type
|
||||
{resource: ResourceGroup.WithID(wid), actions: []policy.Action{policy.ActionRead}},
|
||||
|
||||
// no empty id for the create action
|
||||
{resource: ResourceGroup.InOrg(defOrg), actions: []policy.Action{policy.ActionCreate}},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
// Wildcard type
|
||||
user = Subject{
|
||||
ID: "me",
|
||||
Roles: Roles{
|
||||
must(RoleByName(RoleOwner())),
|
||||
},
|
||||
Scope: Scope{
|
||||
Role: Role{
|
||||
Identifier: RoleIdentifier{
|
||||
Name: "WildcardType",
|
||||
OrganizationID: defOrg,
|
||||
},
|
||||
DisplayName: "WildcardType",
|
||||
// Allow almost everything
|
||||
Site: allPermsExcept(ResourceUser),
|
||||
},
|
||||
AllowIDList: []AllowListElement{
|
||||
{Type: policy.WildcardSymbol, ID: wid.String()},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testAuthorize(t, "WildcardType", user,
|
||||
// Allowed:
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.allow = true
|
||||
return c
|
||||
},
|
||||
[]authTestCase{
|
||||
// anything with the id is ok
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID).WithID(wid), actions: []policy.Action{policy.ActionRead}},
|
||||
{resource: ResourceGroup.InOrg(defOrg).WithID(wid), actions: []policy.Action{policy.ActionRead}},
|
||||
{resource: ResourceTemplate.InOrg(defOrg).WithID(wid), actions: []policy.Action{policy.ActionRead}},
|
||||
},
|
||||
),
|
||||
|
||||
// Not allowed:
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.allow = false
|
||||
return c
|
||||
},
|
||||
[]authTestCase{
|
||||
// Anything without the id is not allowed
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionCreate}},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID).WithID(uuid.New()), actions: []policy.Action{policy.ActionRead}},
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"action": "never-match-action",
|
||||
"action": "read",
|
||||
"object": {
|
||||
"id": "9046b041-58ed-47a3-9c3a-de302577875a",
|
||||
"owner": "00000000-0000-0000-0000-000000000000",
|
||||
@@ -40,7 +40,11 @@
|
||||
],
|
||||
"org": {},
|
||||
"user": [],
|
||||
"allow_list": ["*"]
|
||||
"allow_list": [
|
||||
{
|
||||
"type": "workspace",
|
||||
"id": "*"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-5
@@ -246,18 +246,39 @@ user_allow(roles) := num if {
|
||||
num := number(allow)
|
||||
}
|
||||
|
||||
# Scope allow_list is a list of resource IDs explicitly allowed by the scope.
|
||||
# If the list is '*', then all resources are allowed.
|
||||
# Scope allow_list is a list of resource (Type, ID) tuples explicitly allowed by the scope.
|
||||
# If the list contains `(*,*)`, then all resources are allowed.
|
||||
scope_allow_list if {
|
||||
"*" in input.subject.scope.allow_list
|
||||
input.subject.scope.allow_list[_] == {"type": "*", "id": "*"}
|
||||
}
|
||||
|
||||
# This is a shortcut if the allow_list contains (type, *), then allow all IDs of that type.
|
||||
scope_allow_list if {
|
||||
input.subject.scope.allow_list[_] == {"type": input.object.type, "id": "*"}
|
||||
}
|
||||
|
||||
# A comprehension that iterates over the allow_list and checks if the
|
||||
# (object.type, object.id) is in the allowed ids.
|
||||
scope_allow_list if {
|
||||
# If the wildcard is listed in the allow_list, we do not care about the
|
||||
# object.id. This line is included to prevent partial compilations from
|
||||
# ever needing to include the object.id.
|
||||
not "*" in input.subject.scope.allow_list
|
||||
input.object.id in input.subject.scope.allow_list
|
||||
not {"type": "*", "id": "*"} in input.subject.scope.allow_list
|
||||
# This is equivalent to the above line, as `type` is known at partial query time.
|
||||
not {"type": input.object.type, "id": "*"} in input.subject.scope.allow_list
|
||||
|
||||
# allows_ids is the set of all ids allowed for the given object.type
|
||||
allowed_ids := {allowed_id |
|
||||
# Iterate over all allow list elements
|
||||
ele := input.subject.scope.allow_list[_]
|
||||
ele.type in [input.object.type, "*"]
|
||||
allowed_id := ele.id
|
||||
}
|
||||
|
||||
# Return if the object.id is in the allowed ids
|
||||
# This rule is evaluated at the end so the partial query can use the object.id
|
||||
# against this precomputed set of allowed ids.
|
||||
input.object.id in allowed_ids
|
||||
}
|
||||
|
||||
# -------------------
|
||||
|
||||
+23
-12
@@ -45,14 +45,15 @@ func WorkspaceAgentScope(params WorkspaceAgentScopeParams) Scope {
|
||||
// incase we change the behavior of the allowlist. The allowlist is new
|
||||
// and evolving.
|
||||
Role: scope.Role,
|
||||
// This prevents the agent from being able to access any other resource.
|
||||
// Include the list of IDs of anything that is required for the
|
||||
// agent to function.
|
||||
AllowIDList: []string{
|
||||
params.WorkspaceID.String(),
|
||||
params.TemplateID.String(),
|
||||
params.VersionID.String(),
|
||||
params.OwnerID.String(),
|
||||
|
||||
// Limit the agent to only be able to access the singular workspace and
|
||||
// the template/version it was created from. Add additional resources here
|
||||
// as needed, but do not add more workspace or template resource ids.
|
||||
AllowIDList: []AllowListElement{
|
||||
{Type: ResourceWorkspace.Type, ID: params.WorkspaceID.String()},
|
||||
{Type: ResourceTemplate.Type, ID: params.TemplateID.String()},
|
||||
{Type: ResourceTemplate.Type, ID: params.VersionID.String()},
|
||||
{Type: ResourceUser.Type, ID: params.OwnerID.String()},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -77,7 +78,7 @@ var builtinScopes = map[ScopeName]Scope{
|
||||
Org: map[string][]Permission{},
|
||||
User: []Permission{},
|
||||
},
|
||||
AllowIDList: []string{policy.WildcardSymbol},
|
||||
AllowIDList: []AllowListElement{AllowListAll()},
|
||||
},
|
||||
|
||||
ScopeApplicationConnect: {
|
||||
@@ -90,7 +91,7 @@ var builtinScopes = map[ScopeName]Scope{
|
||||
Org: map[string][]Permission{},
|
||||
User: []Permission{},
|
||||
},
|
||||
AllowIDList: []string{policy.WildcardSymbol},
|
||||
AllowIDList: []AllowListElement{AllowListAll()},
|
||||
},
|
||||
|
||||
ScopeNoUserData: {
|
||||
@@ -101,7 +102,7 @@ var builtinScopes = map[ScopeName]Scope{
|
||||
Org: map[string][]Permission{},
|
||||
User: []Permission{},
|
||||
},
|
||||
AllowIDList: []string{policy.WildcardSymbol},
|
||||
AllowIDList: []AllowListElement{AllowListAll()},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -129,7 +130,17 @@ func (name ScopeName) Name() RoleIdentifier {
|
||||
// AllowIDList. Eg: 'AllowIDList: []string{WildcardSymbol}'
|
||||
type Scope struct {
|
||||
Role
|
||||
AllowIDList []string `json:"allow_list"`
|
||||
AllowIDList []AllowListElement `json:"allow_list"`
|
||||
}
|
||||
|
||||
type AllowListElement struct {
|
||||
// ID must be a string to allow for the wildcard symbol.
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func AllowListAll() AllowListElement {
|
||||
return AllowListElement{ID: policy.WildcardSymbol, Type: policy.WildcardSymbol}
|
||||
}
|
||||
|
||||
func (s Scope) Expand() (Scope, error) {
|
||||
|
||||
Reference in New Issue
Block a user