Access Control: Support other attributes than id for resource permissions (#46727)

* Add option to set ResourceAttribute for a permissions service
* Use prefix in access control sql filter to parse scopes
* Use prefix in access control metadata to check access
This commit is contained in:
Karl Persson
2022-03-21 17:58:18 +01:00
committed by GitHub
parent 79f5c7d7a7
commit 7ab1ef8d6e
26 changed files with 363 additions and 288 deletions

View File

@@ -1,8 +1,6 @@
package api package api
import ( import (
"fmt"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol" ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
@@ -463,15 +461,13 @@ var teamsEditAccessEvaluator = ac.EvalAll(
// Metadata helpers // Metadata helpers
// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource // getAccessControlMetadata returns the accesscontrol metadata associated with a given resource
func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, resource string, id int64) ac.Metadata { func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, prefix string, resourceID string) ac.Metadata {
key := fmt.Sprintf("%d", id) ids := map[string]bool{resourceID: true}
ids := map[string]bool{key: true} return hs.getMultiAccessControlMetadata(c, prefix, ids)[resourceID]
return hs.getMultiAccessControlMetadata(c, resource, ids)[key]
} }
// getMultiAccessControlMetadata returns the accesscontrol metadata associated with a given set of resources // getMultiAccessControlMetadata returns the accesscontrol metadata associated with a given set of resources
func (hs *HTTPServer) getMultiAccessControlMetadata(c *models.ReqContext, resource string, ids map[string]bool) map[string]ac.Metadata { func (hs *HTTPServer) getMultiAccessControlMetadata(c *models.ReqContext, prefix string, resourceIDs map[string]bool) map[string]ac.Metadata {
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
return map[string]ac.Metadata{} return map[string]ac.Metadata{}
} }
@@ -485,5 +481,5 @@ func (hs *HTTPServer) getMultiAccessControlMetadata(c *models.ReqContext, resour
return map[string]ac.Metadata{} return map[string]ac.Metadata{}
} }
return ac.GetResourcesMetadata(c.Req.Context(), permissions, resource, ids) return ac.GetResourcesMetadata(c.Req.Context(), permissions, prefix, resourceIDs)
} }

View File

@@ -100,7 +100,7 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response
dto := convertModelToDtos(filtered[0]) dto := convertModelToDtos(filtered[0])
// Add accesscontrol metadata // Add accesscontrol metadata
dto.AccessControl = hs.getAccessControlMetadata(c, "datasources", dto.Id) dto.AccessControl = hs.getAccessControlMetadata(c, "datasources:id:", strconv.FormatInt(dto.Id, 10))
return response.JSON(200, &dto) return response.JSON(200, &dto)
} }
@@ -159,8 +159,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response
dto := convertModelToDtos(filtered[0]) dto := convertModelToDtos(filtered[0])
// Add accesscontrol metadata // Add accesscontrol metadata
dto.AccessControl = hs.getAccessControlMetadata(c, "datasources", dto.Id) dto.AccessControl = hs.getAccessControlMetadata(c, "datasources:id:", strconv.FormatInt(dto.Id, 10))
return response.JSON(200, &dto) return response.JSON(200, &dto)
} }

View File

@@ -150,7 +150,7 @@ func (hs *HTTPServer) getOrgUsersHelper(c *models.ReqContext, query *models.GetO
filteredUsers = append(filteredUsers, user) filteredUsers = append(filteredUsers, user)
} }
accessControlMetadata := hs.getMultiAccessControlMetadata(c, "users", userIDs) accessControlMetadata := hs.getMultiAccessControlMetadata(c, "users:id:", userIDs)
if len(accessControlMetadata) > 0 { if len(accessControlMetadata) > 0 {
for i := range filteredUsers { for i := range filteredUsers {
filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserId)] filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserId)]

View File

@@ -141,7 +141,7 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
teamIDs[strconv.FormatInt(team.Id, 10)] = true teamIDs[strconv.FormatInt(team.Id, 10)] = true
} }
metadata := hs.getMultiAccessControlMetadata(c, "teams", teamIDs) metadata := hs.getMultiAccessControlMetadata(c, "teams:id:", teamIDs)
if len(metadata) > 0 { if len(metadata) > 0 {
for _, team := range query.Result.Teams { for _, team := range query.Result.Teams {
team.AccessControl = metadata[strconv.FormatInt(team.Id, 10)] team.AccessControl = metadata[strconv.FormatInt(team.Id, 10)]
@@ -195,7 +195,7 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
} }
// Add accesscontrol metadata // Add accesscontrol metadata
query.Result.AccessControl = hs.getAccessControlMetadata(c, "teams", query.Result.Id) query.Result.AccessControl = hs.getAccessControlMetadata(c, "teams:id:", strconv.FormatInt(query.Result.Id, 10))
query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name) query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name)
return response.JSON(200, &query.Result) return response.JSON(200, &query.Result)

View File

@@ -46,7 +46,7 @@ func (hs *HTTPServer) getUserUserProfile(c *models.ReqContext, userID int64) res
query.Result.IsExternal = true query.Result.IsExternal = true
} }
query.Result.AccessControl = hs.getAccessControlMetadata(c, "global:users", userID) query.Result.AccessControl = hs.getAccessControlMetadata(c, "global:users:id:", strconv.FormatInt(userID, 10))
query.Result.AvatarUrl = dtos.GetGravatarUrl(query.Result.Email) query.Result.AvatarUrl = dtos.GetGravatarUrl(query.Result.Email)
return response.JSON(200, query.Result) return response.JSON(200, query.Result)

View File

@@ -164,28 +164,32 @@ func addActionToMetadata(allMetadata map[string]Metadata, action, id string) map
} }
// GetResourcesMetadata returns a map of accesscontrol metadata, listing for each resource, users available actions // GetResourcesMetadata returns a map of accesscontrol metadata, listing for each resource, users available actions
func GetResourcesMetadata(ctx context.Context, permissions map[string][]string, resource string, ids map[string]bool) map[string]Metadata { func GetResourcesMetadata(ctx context.Context, permissions map[string][]string, prefix string, resourceIDs map[string]bool) map[string]Metadata {
allScope := GetResourceAllScope(resource) rootPrefix, attributePrefix, ok := extractPrefixes(prefix)
allIDScope := GetResourceAllIDScope(resource) if !ok {
return map[string]Metadata{}
}
// prefix of ID based scopes (resource:id) allScope := GetResourceAllScope(strings.TrimSuffix(rootPrefix, ":"))
idPrefix := Scope(resource, "id") allAttributeScope := Scope(strings.TrimSuffix(attributePrefix, ":"), "*")
// index of the ID in the scope
idIndex := len(idPrefix) + 1 // index of the attribute in the scope
attributeIndex := len(attributePrefix)
// Loop through permissions once // Loop through permissions once
result := map[string]Metadata{} result := map[string]Metadata{}
for action, scopes := range permissions { for action, scopes := range permissions {
for _, scope := range scopes { for _, scope := range scopes {
if scope == "*" || scope == allScope || scope == allIDScope { if scope == "*" || scope == allScope || scope == allAttributeScope {
// Add global action to all resources // Add global action to all resources
for id := range ids { for id := range resourceIDs {
result = addActionToMetadata(result, action, id) result = addActionToMetadata(result, action, id)
} }
} else { } else {
if len(scope) > idIndex && strings.HasPrefix(scope, idPrefix) && ids[scope[idIndex:]] { if len(scope) > attributeIndex && strings.HasPrefix(scope, attributePrefix) && resourceIDs[scope[attributeIndex:]] {
// Add action to a specific resource // Add action to a specific resource
result = addActionToMetadata(result, action, scope[idIndex:]) result = addActionToMetadata(result, action, scope[attributeIndex:])
} }
} }
} }
@@ -205,3 +209,13 @@ func ManagedTeamRoleName(teamID int64) string {
func ManagedBuiltInRoleName(builtInRole string) string { func ManagedBuiltInRoleName(builtInRole string) string {
return fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(builtInRole)) return fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(builtInRole))
} }
func extractPrefixes(prefix string) (string, string, bool) {
parts := strings.Split(strings.TrimSuffix(prefix, ":"), ":")
if len(parts) != 2 {
return "", "", false
}
rootPrefix := parts[0] + ":"
attributePrefix := rootPrefix + parts[1] + ":"
return rootPrefix, attributePrefix, true
}

View File

@@ -31,7 +31,7 @@ func benchGetMetadata(b *testing.B, resourceCount, permissionPerResource int) {
var metadata map[string]Metadata var metadata map[string]Metadata
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
metadata = GetResourcesMetadata(context.Background(), permissions, "resources", ids) metadata = GetResourcesMetadata(context.Background(), permissions, "resources:id:", ids)
assert.Len(b, metadata, resourceCount) assert.Len(b, metadata, resourceCount)
for _, resourceMetadata := range metadata { for _, resourceMetadata := range metadata {
assert.Len(b, resourceMetadata, permissionPerResource) assert.Len(b, resourceMetadata, permissionPerResource)

View File

@@ -10,20 +10,20 @@ import (
func TestGetResourcesMetadata(t *testing.T) { func TestGetResourcesMetadata(t *testing.T) {
tests := []struct { tests := []struct {
desc string desc string
resource string prefix string
resourcesIDs map[string]bool resourcesIDs map[string]bool
permissions map[string][]string permissions map[string][]string
expected map[string]Metadata expected map[string]Metadata
}{ }{
{ {
desc: "Should return no permission for resources 1,2,3 given the user has no permission", desc: "Should return no permission for resources 1,2,3 given the user has no permission",
resource: "resources", prefix: "resources:id:",
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true}, resourcesIDs: map[string]bool{"1": true, "2": true, "3": true},
expected: map[string]Metadata{}, expected: map[string]Metadata{},
}, },
{ {
desc: "Should return no permission for resources 1,2,3 given the user has permissions for 4 only", desc: "Should return no permission for resources 1,2,3 given the user has permissions for 4 only",
resource: "resources", prefix: "resources:id:",
permissions: map[string][]string{ permissions: map[string][]string{
"resources:action1": {Scope("resources", "id", "4")}, "resources:action1": {Scope("resources", "id", "4")},
"resources:action2": {Scope("resources", "id", "4")}, "resources:action2": {Scope("resources", "id", "4")},
@@ -33,8 +33,8 @@ func TestGetResourcesMetadata(t *testing.T) {
expected: map[string]Metadata{}, expected: map[string]Metadata{},
}, },
{ {
desc: "Should only return permissions for resources 1 and 2, given the user has no permissions for 3", desc: "Should only return permissions for resources 1 and 2, given the user has no permissions for 3",
resource: "resources", prefix: "resources:id:",
permissions: map[string][]string{ permissions: map[string][]string{
"resources:action1": {Scope("resources", "id", "1")}, "resources:action1": {Scope("resources", "id", "1")},
"resources:action2": {Scope("resources", "id", "2")}, "resources:action2": {Scope("resources", "id", "2")},
@@ -47,8 +47,8 @@ func TestGetResourcesMetadata(t *testing.T) {
}, },
}, },
{ {
desc: "Should return permissions with global scopes for resources 1,2,3", desc: "Should return permissions with global scopes for resources 1,2,3",
resource: "resources", prefix: "resources:id:",
permissions: map[string][]string{ permissions: map[string][]string{
"resources:action1": {Scope("resources", "id", "1")}, "resources:action1": {Scope("resources", "id", "1")},
"resources:action2": {Scope("resources", "id", "2")}, "resources:action2": {Scope("resources", "id", "2")},
@@ -65,8 +65,8 @@ func TestGetResourcesMetadata(t *testing.T) {
}, },
}, },
{ {
desc: "Should correctly filter out irrelevant permissions for resources 1,2,3", desc: "Should correctly filter out irrelevant permissions for resources 1,2,3",
resource: "resources", prefix: "resources:id:",
permissions: map[string][]string{ permissions: map[string][]string{
"resources:action1": {Scope("resources", "id", "1")}, "resources:action1": {Scope("resources", "id", "1")},
"resources:action2": {Scope("otherresources", "id", "*")}, "resources:action2": {Scope("otherresources", "id", "*")},
@@ -77,22 +77,10 @@ func TestGetResourcesMetadata(t *testing.T) {
"1": {"resources:action1": true, "otherresources:action1": true}, "1": {"resources:action1": true, "otherresources:action1": true},
}, },
}, },
{
desc: "Should correctly handle permissions with multilayer scope",
resource: "resources:sub",
permissions: map[string][]string{
"resources:action1": {Scope("resources", "sub", "id", "1"), Scope("resources", "sub", "id", "123")},
},
resourcesIDs: map[string]bool{"1": true, "123": true},
expected: map[string]Metadata{
"1": {"resources:action1": true},
"123": {"resources:action1": true},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
metadata := GetResourcesMetadata(context.Background(), tt.permissions, tt.resource, tt.resourcesIDs) metadata := GetResourcesMetadata(context.Background(), tt.permissions, tt.prefix, tt.resourcesIDs)
assert.EqualValues(t, tt.expected, metadata) assert.EqualValues(t, tt.expected, metadata)
}) })
} }

View File

@@ -210,7 +210,7 @@ func (s *AccessControlStore) setResourcePermission(
` `
var current []accesscontrol.Permission var current []accesscontrol.Permission
if err := sess.SQL(rawSQL, role.ID, accesscontrol.GetResourceScope(cmd.Resource, cmd.ResourceID)).Find(&current); err != nil { if err := sess.SQL(rawSQL, role.ID, accesscontrol.Scope(cmd.Resource, cmd.ResourceAttribute, cmd.ResourceID)).Find(&current); err != nil {
return nil, err return nil, err
} }
@@ -235,14 +235,14 @@ func (s *AccessControlStore) setResourcePermission(
} }
for action := range missing { for action := range missing {
id, err := s.createResourcePermission(sess, role.ID, action, cmd.Resource, cmd.ResourceID) id, err := s.createResourcePermission(sess, role.ID, action, cmd.Resource, cmd.ResourceID, cmd.ResourceAttribute)
if err != nil { if err != nil {
return nil, err return nil, err
} }
keep = append(keep, id) keep = append(keep, id)
} }
permissions, err := s.getResourcePermissionsByIds(sess, cmd.Resource, cmd.ResourceID, keep) permissions, err := s.getResourcePermissionsByIds(sess, cmd.Resource, cmd.ResourceID, cmd.ResourceAttribute, keep)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -267,8 +267,8 @@ func (s *AccessControlStore) GetResourcePermissions(ctx context.Context, orgID i
return result, err return result, err
} }
func (s *AccessControlStore) createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, resource string, resourceID string) (int64, error) { func (s *AccessControlStore) createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, resource, resourceID, resourceAttribute string) (int64, error) {
permission := managedPermission(action, resource, resourceID) permission := managedPermission(action, resource, resourceID, resourceAttribute)
permission.RoleID = roleID permission.RoleID = roleID
permission.Created = time.Now() permission.Created = time.Now()
permission.Updated = time.Now() permission.Updated = time.Now()
@@ -340,14 +340,14 @@ func (s *AccessControlStore) getResourcePermissions(sess *sqlstore.DBSession, or
where := `WHERE (r.org_id = ? OR r.org_id = 0) AND (p.scope = '*' OR p.scope = ? OR p.scope = ? OR p.scope = ?` where := `WHERE (r.org_id = ? OR r.org_id = 0) AND (p.scope = '*' OR p.scope = ? OR p.scope = ? OR p.scope = ?`
scope := accesscontrol.GetResourceScope(query.Resource, query.ResourceID) scope := accesscontrol.Scope(query.Resource, query.ResourceAttribute, query.ResourceID)
args := []interface{}{ args := []interface{}{
scope, scope,
orgID, orgID,
orgID, orgID,
accesscontrol.GetResourceAllScope(query.Resource), accesscontrol.Scope(query.Resource, "*"),
accesscontrol.GetResourceAllIDScope(query.Resource), accesscontrol.Scope(query.Resource, query.ResourceAttribute, "*"),
scope, scope,
} }
@@ -370,14 +370,14 @@ func (s *AccessControlStore) getResourcePermissions(sess *sqlstore.DBSession, or
initialLength := len(args) initialLength := len(args)
userFilter, err := accesscontrol.Filter(query.User, "u.id", "users", accesscontrol.ActionOrgUsersRead) userFilter, err := accesscontrol.Filter(query.User, "u.id", "users:id:", accesscontrol.ActionOrgUsersRead)
if err != nil { if err != nil {
return nil, err return nil, err
} }
user := userSelect + userFrom + where + " AND " + userFilter.Where user := userSelect + userFrom + where + " AND " + userFilter.Where
args = append(args, userFilter.Args...) args = append(args, userFilter.Args...)
teamFilter, err := accesscontrol.Filter(query.User, "t.id", "teams", accesscontrol.ActionTeamsRead) teamFilter, err := accesscontrol.Filter(query.User, "t.id", "teams:id:", accesscontrol.ActionTeamsRead)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -574,7 +574,7 @@ func (s *AccessControlStore) getOrCreateManagedRole(sess *sqlstore.DBSession, or
return &role, nil return &role, nil
} }
func (s *AccessControlStore) getResourcePermissionsByIds(sess *sqlstore.DBSession, resource, resourceID string, ids []int64) ([]flatResourcePermission, error) { func (s *AccessControlStore) getResourcePermissionsByIds(sess *sqlstore.DBSession, resource, resourceID, resourceAttribute string, ids []int64) ([]flatResourcePermission, error) {
var result []flatResourcePermission var result []flatResourcePermission
if len(ids) == 0 { if len(ids) == 0 {
return result, nil return result, nil
@@ -602,7 +602,7 @@ func (s *AccessControlStore) getResourcePermissionsByIds(sess *sqlstore.DBSessio
` `
args := make([]interface{}, 0, len(ids)+1) args := make([]interface{}, 0, len(ids)+1)
args = append(args, accesscontrol.GetResourceScope(resource, resourceID)) args = append(args, accesscontrol.Scope(resource, resourceAttribute, resourceID))
for _, id := range ids { for _, id := range ids {
args = append(args, id) args = append(args, id)
} }
@@ -614,9 +614,9 @@ func (s *AccessControlStore) getResourcePermissionsByIds(sess *sqlstore.DBSessio
return result, nil return result, nil
} }
func managedPermission(action, resource string, resourceID string) accesscontrol.Permission { func managedPermission(action, resource string, resourceID, resourceAttribute string) accesscontrol.Permission {
return accesscontrol.Permission{ return accesscontrol.Permission{
Action: action, Action: action,
Scope: accesscontrol.GetResourceScope(resource, resourceID), Scope: accesscontrol.Scope(resource, resourceAttribute, resourceID),
} }
} }

View File

@@ -56,9 +56,11 @@ func getDSPermissions(b *testing.B, store *AccessControlStore, dataSources []int
dsId := dataSources[0] dsId := dataSources[0]
permissions, err := store.GetResourcePermissions(context.Background(), accesscontrol.GlobalOrgID, types.GetResourcePermissionsQuery{ permissions, err := store.GetResourcePermissions(context.Background(), accesscontrol.GlobalOrgID, types.GetResourcePermissionsQuery{
Actions: []string{dsAction}, User: &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: {"org.users:read": {"users:*"}, "teams:read": {"teams:*"}}}},
Resource: dsResource, Actions: []string{dsAction},
ResourceID: strconv.Itoa(int(dsId)), Resource: dsResource,
ResourceID: strconv.Itoa(int(dsId)),
ResourceAttribute: "id",
}) })
require.NoError(b, err) require.NoError(b, err)
assert.GreaterOrEqual(b, len(permissions), 2) assert.GreaterOrEqual(b, len(permissions), 2)
@@ -96,9 +98,10 @@ func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *Acce
accesscontrol.GlobalOrgID, accesscontrol.GlobalOrgID,
accesscontrol.User{ID: userIds[i]}, accesscontrol.User{ID: userIds[i]},
types.SetResourcePermissionCommand{ types.SetResourcePermissionCommand{
Actions: []string{dsAction}, Actions: []string{dsAction},
Resource: dsResource, Resource: dsResource,
ResourceID: strconv.Itoa(int(dsID)), ResourceID: strconv.Itoa(int(dsID)),
ResourceAttribute: "id",
}, },
nil, nil,
) )
@@ -113,9 +116,10 @@ func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *Acce
accesscontrol.GlobalOrgID, accesscontrol.GlobalOrgID,
teamIds[i], teamIds[i],
types.SetResourcePermissionCommand{ types.SetResourcePermissionCommand{
Actions: []string{"datasources:query"}, Actions: []string{"datasources:query"},
Resource: "datasources", Resource: "datasources",
ResourceID: strconv.Itoa(int(dsID)), ResourceID: strconv.Itoa(int(dsID)),
ResourceAttribute: "id",
}, },
nil, nil,
) )

View File

@@ -16,31 +16,34 @@ import (
) )
type setUserResourcePermissionTest struct { type setUserResourcePermissionTest struct {
desc string desc string
orgID int64 orgID int64
userID int64 userID int64
actions []string actions []string
resource string resource string
resourceID string resourceID string
seeds []types.SetResourcePermissionCommand resourceAttribute string
seeds []types.SetResourcePermissionCommand
} }
func TestAccessControlStore_SetUserResourcePermission(t *testing.T) { func TestAccessControlStore_SetUserResourcePermission(t *testing.T) {
tests := []setUserResourcePermissionTest{ tests := []setUserResourcePermissionTest{
{ {
desc: "should set resource permission for user", desc: "should set resource permission for user",
userID: 1, userID: 1,
actions: []string{"datasources:query"}, actions: []string{"datasources:query"},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
resourceAttribute: "uid",
}, },
{ {
desc: "should remove resource permission for user", desc: "should remove resource permission for user",
orgID: 1, orgID: 1,
userID: 1, userID: 1,
actions: []string{}, actions: []string{},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
resourceAttribute: "uid",
seeds: []types.SetResourcePermissionCommand{ seeds: []types.SetResourcePermissionCommand{
{ {
Actions: []string{"datasources:query"}, Actions: []string{"datasources:query"},
@@ -50,12 +53,13 @@ func TestAccessControlStore_SetUserResourcePermission(t *testing.T) {
}, },
}, },
{ {
desc: "should add new resource permission for user", desc: "should add new resource permission for user",
orgID: 1, orgID: 1,
userID: 1, userID: 1,
actions: []string{"datasources:query", "datasources:write"}, actions: []string{"datasources:query", "datasources:write"},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
resourceAttribute: "uid",
seeds: []types.SetResourcePermissionCommand{ seeds: []types.SetResourcePermissionCommand{
{ {
Actions: []string{"datasources:write"}, Actions: []string{"datasources:write"},
@@ -76,9 +80,10 @@ func TestAccessControlStore_SetUserResourcePermission(t *testing.T) {
} }
added, err := store.SetUserResourcePermission(context.Background(), test.userID, accesscontrol.User{ID: test.userID}, types.SetResourcePermissionCommand{ added, err := store.SetUserResourcePermission(context.Background(), test.userID, accesscontrol.User{ID: test.userID}, types.SetResourcePermissionCommand{
Actions: test.actions, Actions: test.actions,
Resource: test.resource, Resource: test.resource,
ResourceID: test.resourceID, ResourceID: test.resourceID,
ResourceAttribute: test.resourceAttribute,
}, nil) }, nil)
require.NoError(t, err) require.NoError(t, err)
@@ -86,59 +91,65 @@ func TestAccessControlStore_SetUserResourcePermission(t *testing.T) {
assert.Equal(t, accesscontrol.ResourcePermission{}, *added) assert.Equal(t, accesscontrol.ResourcePermission{}, *added)
} else { } else {
assert.Len(t, added.Actions, len(test.actions)) assert.Len(t, added.Actions, len(test.actions))
assert.Equal(t, accesscontrol.GetResourceScope(test.resource, test.resourceID), added.Scope) assert.Equal(t, accesscontrol.Scope(test.resource, test.resourceAttribute, test.resourceID), added.Scope)
} }
}) })
} }
} }
type setTeamResourcePermissionTest struct { type setTeamResourcePermissionTest struct {
desc string desc string
orgID int64 orgID int64
teamID int64 teamID int64
actions []string actions []string
resource string resource string
resourceID string resourceID string
seeds []types.SetResourcePermissionCommand resourceAttribute string
seeds []types.SetResourcePermissionCommand
} }
func TestAccessControlStore_SetTeamResourcePermission(t *testing.T) { func TestAccessControlStore_SetTeamResourcePermission(t *testing.T) {
tests := []setTeamResourcePermissionTest{ tests := []setTeamResourcePermissionTest{
{ {
desc: "should add new resource permission for team", desc: "should add new resource permission for team",
orgID: 1, orgID: 1,
teamID: 1, teamID: 1,
actions: []string{"datasources:query"}, actions: []string{"datasources:query"},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
resourceAttribute: "uid",
}, },
{ {
desc: "should add new resource permission when others exist", desc: "should add new resource permission when others exist",
orgID: 1, orgID: 1,
teamID: 1, teamID: 1,
actions: []string{"datasources:query", "datasources:write"}, actions: []string{"datasources:query", "datasources:write"},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
resourceAttribute: "uid",
seeds: []types.SetResourcePermissionCommand{ seeds: []types.SetResourcePermissionCommand{
{ {
Actions: []string{"datasources:query"}, Actions: []string{"datasources:query"},
Resource: "datasources", Resource: "datasources",
ResourceID: "1", ResourceID: "1",
ResourceAttribute: "uid",
}, },
}, },
}, },
{ {
desc: "should remove permissions for team", desc: "should remove permissions for team",
orgID: 1, orgID: 1,
teamID: 1, teamID: 1,
actions: []string{}, actions: []string{},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
resourceAttribute: "uid",
seeds: []types.SetResourcePermissionCommand{ seeds: []types.SetResourcePermissionCommand{
{ {
Actions: []string{"datasources:query"}, Actions: []string{"datasources:query"},
Resource: "datasources", Resource: "datasources",
ResourceID: "1", ResourceID: "1",
ResourceAttribute: "uid",
}, },
}, },
}, },
@@ -154,9 +165,10 @@ func TestAccessControlStore_SetTeamResourcePermission(t *testing.T) {
} }
added, err := store.SetTeamResourcePermission(context.Background(), test.orgID, test.teamID, types.SetResourcePermissionCommand{ added, err := store.SetTeamResourcePermission(context.Background(), test.orgID, test.teamID, types.SetResourcePermissionCommand{
Actions: test.actions, Actions: test.actions,
Resource: test.resource, Resource: test.resource,
ResourceID: test.resourceID, ResourceID: test.resourceID,
ResourceAttribute: test.resourceAttribute,
}, nil) }, nil)
require.NoError(t, err) require.NoError(t, err)
@@ -164,59 +176,65 @@ func TestAccessControlStore_SetTeamResourcePermission(t *testing.T) {
assert.Equal(t, accesscontrol.ResourcePermission{}, *added) assert.Equal(t, accesscontrol.ResourcePermission{}, *added)
} else { } else {
assert.Len(t, added.Actions, len(test.actions)) assert.Len(t, added.Actions, len(test.actions))
assert.Equal(t, accesscontrol.GetResourceScope(test.resource, test.resourceID), added.Scope) assert.Equal(t, accesscontrol.Scope(test.resource, test.resourceAttribute, test.resourceID), added.Scope)
} }
}) })
} }
} }
type setBuiltInResourcePermissionTest struct { type setBuiltInResourcePermissionTest struct {
desc string desc string
orgID int64 orgID int64
builtInRole string builtInRole string
actions []string actions []string
resource string resource string
resourceID string resourceID string
seeds []types.SetResourcePermissionCommand resourceAttribute string
seeds []types.SetResourcePermissionCommand
} }
func TestAccessControlStore_SetBuiltInResourcePermission(t *testing.T) { func TestAccessControlStore_SetBuiltInResourcePermission(t *testing.T) {
tests := []setBuiltInResourcePermissionTest{ tests := []setBuiltInResourcePermissionTest{
{ {
desc: "should add new resource permission for builtin role", desc: "should add new resource permission for builtin role",
orgID: 1, orgID: 1,
builtInRole: "Viewer", builtInRole: "Viewer",
actions: []string{"datasources:query"}, actions: []string{"datasources:query"},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
resourceAttribute: "uid",
}, },
{ {
desc: "should add new resource permission when others exist", desc: "should add new resource permission when others exist",
orgID: 1, orgID: 1,
builtInRole: "Viewer", builtInRole: "Viewer",
actions: []string{"datasources:query", "datasources:write"}, actions: []string{"datasources:query", "datasources:write"},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
resourceAttribute: "uid",
seeds: []types.SetResourcePermissionCommand{ seeds: []types.SetResourcePermissionCommand{
{ {
Actions: []string{"datasources:query"}, Actions: []string{"datasources:query"},
Resource: "datasources", Resource: "datasources",
ResourceID: "1", ResourceID: "1",
ResourceAttribute: "uid",
}, },
}, },
}, },
{ {
desc: "should remove permissions for builtin role", desc: "should remove permissions for builtin role",
orgID: 1, orgID: 1,
builtInRole: "Viewer", builtInRole: "Viewer",
actions: []string{}, actions: []string{},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
resourceAttribute: "uid",
seeds: []types.SetResourcePermissionCommand{ seeds: []types.SetResourcePermissionCommand{
{ {
Actions: []string{"datasources:query"}, Actions: []string{"datasources:query"},
Resource: "datasources", Resource: "datasources",
ResourceID: "1", ResourceID: "1",
ResourceAttribute: "uid",
}, },
}, },
}, },
@@ -232,9 +250,10 @@ func TestAccessControlStore_SetBuiltInResourcePermission(t *testing.T) {
} }
added, err := store.SetBuiltInResourcePermission(context.Background(), test.orgID, test.builtInRole, types.SetResourcePermissionCommand{ added, err := store.SetBuiltInResourcePermission(context.Background(), test.orgID, test.builtInRole, types.SetResourcePermissionCommand{
Actions: test.actions, Actions: test.actions,
Resource: test.resource, Resource: test.resource,
ResourceID: test.resourceID, ResourceID: test.resourceID,
ResourceAttribute: test.resourceAttribute,
}, nil) }, nil)
require.NoError(t, err) require.NoError(t, err)
@@ -242,46 +261,51 @@ func TestAccessControlStore_SetBuiltInResourcePermission(t *testing.T) {
assert.Equal(t, accesscontrol.ResourcePermission{}, *added) assert.Equal(t, accesscontrol.ResourcePermission{}, *added)
} else { } else {
assert.Len(t, added.Actions, len(test.actions)) assert.Len(t, added.Actions, len(test.actions))
assert.Equal(t, accesscontrol.GetResourceScope(test.resource, test.resourceID), added.Scope) assert.Equal(t, accesscontrol.Scope(test.resource, test.resourceAttribute, test.resourceID), added.Scope)
} }
}) })
} }
} }
type setResourcePermissionsTest struct { type setResourcePermissionsTest struct {
desc string desc string
orgID int64 orgID int64
commands []types.SetResourcePermissionsCommand resourceAttribute string
commands []types.SetResourcePermissionsCommand
} }
func TestAccessControlStore_SetResourcePermissions(t *testing.T) { func TestAccessControlStore_SetResourcePermissions(t *testing.T) {
tests := []setResourcePermissionsTest{ tests := []setResourcePermissionsTest{
{ {
desc: "should set all permissions provided", desc: "should set all permissions provided",
orgID: 1, orgID: 1,
resourceAttribute: "uid",
commands: []types.SetResourcePermissionsCommand{ commands: []types.SetResourcePermissionsCommand{
{ {
User: accesscontrol.User{ID: 1}, User: accesscontrol.User{ID: 1},
SetResourcePermissionCommand: types.SetResourcePermissionCommand{ SetResourcePermissionCommand: types.SetResourcePermissionCommand{
Actions: []string{"datasources:query"}, Actions: []string{"datasources:query"},
Resource: "datasources", Resource: "datasources",
ResourceID: "1", ResourceID: "1",
ResourceAttribute: "uid",
}, },
}, },
{ {
TeamID: 3, TeamID: 3,
SetResourcePermissionCommand: types.SetResourcePermissionCommand{ SetResourcePermissionCommand: types.SetResourcePermissionCommand{
Actions: []string{"datasources:query"}, Actions: []string{"datasources:query"},
Resource: "datasources", Resource: "datasources",
ResourceID: "1", ResourceID: "1",
ResourceAttribute: "uid",
}, },
}, },
{ {
BuiltinRole: "Admin", BuiltinRole: "Admin",
SetResourcePermissionCommand: types.SetResourcePermissionCommand{ SetResourcePermissionCommand: types.SetResourcePermissionCommand{
Actions: []string{"datasources:query"}, Actions: []string{"datasources:query"},
Resource: "datasources", Resource: "datasources",
ResourceID: "1", ResourceID: "1",
ResourceAttribute: "uid",
}, },
}, },
}, },
@@ -304,7 +328,7 @@ func TestAccessControlStore_SetResourcePermissions(t *testing.T) {
assert.Equal(t, c.TeamID, permissions[i].TeamId) assert.Equal(t, c.TeamID, permissions[i].TeamId)
assert.Equal(t, c.User.ID, permissions[i].UserId) assert.Equal(t, c.User.ID, permissions[i].UserId)
assert.Equal(t, c.BuiltinRole, permissions[i].BuiltInRole) assert.Equal(t, c.BuiltinRole, permissions[i].BuiltInRole)
assert.Equal(t, accesscontrol.GetResourceScope(c.Resource, c.ResourceID), permissions[i].Scope) assert.Equal(t, accesscontrol.Scope(c.Resource, tt.resourceAttribute, c.ResourceID), permissions[i].Scope)
} }
} }
}) })
@@ -312,13 +336,14 @@ func TestAccessControlStore_SetResourcePermissions(t *testing.T) {
} }
type getResourcePermissionsTest struct { type getResourcePermissionsTest struct {
desc string desc string
user *models.SignedInUser user *models.SignedInUser
numUsers int numUsers int
actions []string actions []string
resource string resource string
resourceID string resourceID string
onlyManaged bool resourceAttribute string
onlyManaged bool
} }
func TestAccessControlStore_GetResourcePermissions(t *testing.T) { func TestAccessControlStore_GetResourcePermissions(t *testing.T) {
@@ -330,10 +355,11 @@ func TestAccessControlStore_GetResourcePermissions(t *testing.T) {
Permissions: map[int64]map[string][]string{ Permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}, 1: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}},
}}, }},
numUsers: 3, numUsers: 3,
actions: []string{"datasources:query"}, actions: []string{"datasources:query"},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
resourceAttribute: "uid",
}, },
{ {
desc: "should return manage permissions for all resource ids", desc: "should return manage permissions for all resource ids",
@@ -342,11 +368,12 @@ func TestAccessControlStore_GetResourcePermissions(t *testing.T) {
Permissions: map[int64]map[string][]string{ Permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}, 1: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}},
}}, }},
numUsers: 3, numUsers: 3,
actions: []string{"datasources:query"}, actions: []string{"datasources:query"},
resource: "datasources", resource: "datasources",
resourceID: "1", resourceID: "1",
onlyManaged: true, resourceAttribute: "uid",
onlyManaged: true,
}, },
} }
@@ -389,14 +416,15 @@ func TestAccessControlStore_GetResourcePermissions(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
seedResourcePermissions(t, store, sql, test.actions, test.resource, test.resourceID, test.numUsers) seedResourcePermissions(t, store, sql, test.actions, test.resource, test.resourceID, test.resourceAttribute, test.numUsers)
permissions, err := store.GetResourcePermissions(context.Background(), test.user.OrgId, types.GetResourcePermissionsQuery{ permissions, err := store.GetResourcePermissions(context.Background(), test.user.OrgId, types.GetResourcePermissionsQuery{
User: test.user, User: test.user,
Actions: test.actions, Actions: test.actions,
Resource: test.resource, Resource: test.resource,
ResourceID: test.resourceID, ResourceID: test.resourceID,
OnlyManaged: test.onlyManaged, ResourceAttribute: test.resourceAttribute,
OnlyManaged: test.onlyManaged,
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -409,7 +437,7 @@ func TestAccessControlStore_GetResourcePermissions(t *testing.T) {
} }
} }
func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlstore.SQLStore, actions []string, resource, resourceID string, numUsers int) { func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlstore.SQLStore, actions []string, resource, resourceID, resourceAttribute string, numUsers int) {
t.Helper() t.Helper()
for i := 0; i < numUsers; i++ { for i := 0; i < numUsers; i++ {
org, _ := sql.GetOrgByName("test") org, _ := sql.GetOrgByName("test")
@@ -427,9 +455,10 @@ func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlst
require.NoError(t, err) require.NoError(t, err)
_, err = store.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: u.Id}, types.SetResourcePermissionCommand{ _, err = store.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: u.Id}, types.SetResourcePermissionCommand{
Actions: actions, Actions: actions,
Resource: resource, Resource: resource,
ResourceID: resourceID, ResourceID: resourceID,
ResourceAttribute: resourceAttribute,
}, nil) }, nil)
require.NoError(t, err) require.NoError(t, err)
} }

View File

@@ -32,6 +32,7 @@ type SQLFilter struct {
// Filter creates a where clause to restrict the view of a query based on a users permissions // Filter creates a where clause to restrict the view of a query based on a users permissions
// Scopes that exists for all actions will be parsed and compared against the supplied sqlID // Scopes that exists for all actions will be parsed and compared against the supplied sqlID
// Prefix parameter is the prefix of the scope that we support (e.g. "users:id:")
func Filter(user *models.SignedInUser, sqlID, prefix string, actions ...string) (SQLFilter, error) { func Filter(user *models.SignedInUser, sqlID, prefix string, actions ...string) (SQLFilter, error) {
if _, ok := sqlIDAcceptList[sqlID]; !ok { if _, ok := sqlIDAcceptList[sqlID]; !ok {
return denyQuery, errors.New("sqlID is not in the accept list") return denyQuery, errors.New("sqlID is not in the accept list")
@@ -41,7 +42,7 @@ func Filter(user *models.SignedInUser, sqlID, prefix string, actions ...string)
} }
wildcards := 0 wildcards := 0
result := make(map[int64]int) result := make(map[interface{}]int)
for _, a := range actions { for _, a := range actions {
ids, hasWildcard := parseScopes(prefix, user.Permissions[user.OrgId][a]) ids, hasWildcard := parseScopes(prefix, user.Permissions[user.OrgId][a])
if hasWildcard { if hasWildcard {
@@ -84,14 +85,37 @@ func Filter(user *models.SignedInUser, sqlID, prefix string, actions ...string)
return SQLFilter{query.String(), ids}, nil return SQLFilter{query.String(), ids}, nil
} }
func parseScopes(prefix string, scopes []string) (ids map[int64]struct{}, hasWildcard bool) { func parseScopes(prefix string, scopes []string) (ids map[interface{}]struct{}, hasWildcard bool) {
ids = make(map[int64]struct{}) ids = make(map[interface{}]struct{})
rootPrefix, attributePrefix, ok := extractPrefixes(prefix)
if !ok {
return nil, false
}
parser := parseStringAttribute
if strings.HasSuffix(prefix, "id:") {
parser = parseIntAttribute
}
allScope := rootPrefix + "*"
allAttributeScope := attributePrefix + "*"
for _, scope := range scopes { for _, scope := range scopes {
if strings.HasPrefix(scope, prefix) || scope == "*" { if scope == "*" {
if id := strings.TrimPrefix(scope, prefix); id == "*" || id == ":*" || id == ":id:*" { return nil, true
}
if strings.HasPrefix(scope, rootPrefix) {
if scope == allScope || scope == allAttributeScope {
return nil, true return nil, true
} }
if id, err := parseScopeID(scope); err == nil {
if !strings.HasPrefix(scope, prefix) {
continue
}
if id, err := parser(scope); err == nil {
ids[id] = struct{}{} ids[id] = struct{}{}
} }
} }
@@ -99,10 +123,14 @@ func parseScopes(prefix string, scopes []string) (ids map[int64]struct{}, hasWil
return ids, false return ids, false
} }
func parseScopeID(scope string) (int64, error) { func parseIntAttribute(scope string) (interface{}, error) {
return strconv.ParseInt(scope[strings.LastIndex(scope, ":")+1:], 10, 64) return strconv.ParseInt(scope[strings.LastIndex(scope, ":")+1:], 10, 64)
} }
func parseStringAttribute(scope string) (interface{}, error) {
return scope[strings.LastIndex(scope, ":")+1:], nil
}
// SetAcceptListForTest allow us to mutate the list for blackbox testing // SetAcceptListForTest allow us to mutate the list for blackbox testing
func SetAcceptListForTest(list map[string]struct{}) func() { func SetAcceptListForTest(list map[string]struct{}) func() {
original := sqlIDAcceptList original := sqlIDAcceptList

View File

@@ -34,7 +34,7 @@ func benchmarkFilter(b *testing.B, numDs, numPermissions int) {
acFilter, err := accesscontrol.Filter( acFilter, err := accesscontrol.Filter(
&models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(permissions)}}, &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(permissions)}},
"data_source.id", "data_source.id",
"datasources", "datasources:id:",
"datasources:read", "datasources:read",
) )
require.NoError(b, err) require.NoError(b, err)

View File

@@ -29,7 +29,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect all data sources to be returned", desc: "expect all data sources to be returned",
sqlID: "data_source.id", sqlID: "data_source.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read"}, actions: []string{"datasources:read"},
permissions: map[string][]string{ permissions: map[string][]string{
"datasources:read": {"datasources:*"}, "datasources:read": {"datasources:*"},
@@ -39,7 +39,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect all data sources for wildcard id scope to be returned", desc: "expect all data sources for wildcard id scope to be returned",
sqlID: "data_source.id", sqlID: "data_source.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read"}, actions: []string{"datasources:read"},
permissions: map[string][]string{ permissions: map[string][]string{
"datasources:read": {"datasources:id:*"}, "datasources:read": {"datasources:id:*"},
@@ -49,7 +49,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect all data sources for wildcard scope to be returned", desc: "expect all data sources for wildcard scope to be returned",
sqlID: "data_source.id", sqlID: "data_source.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read"}, actions: []string{"datasources:read"},
permissions: map[string][]string{ permissions: map[string][]string{
"datasources:read": {"*"}, "datasources:read": {"*"},
@@ -59,7 +59,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect no data sources to be returned", desc: "expect no data sources to be returned",
sqlID: "data_source.id", sqlID: "data_source.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read"}, actions: []string{"datasources:read"},
permissions: map[string][]string{}, permissions: map[string][]string{},
expectedDataSources: []string{}, expectedDataSources: []string{},
@@ -67,7 +67,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect data sources with id 3, 7 and 8 to be returned", desc: "expect data sources with id 3, 7 and 8 to be returned",
sqlID: "data_source.id", sqlID: "data_source.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read"}, actions: []string{"datasources:read"},
permissions: map[string][]string{ permissions: map[string][]string{
"datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8"}, "datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8"},
@@ -77,7 +77,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect no data sources to be returned for malformed scope", desc: "expect no data sources to be returned for malformed scope",
sqlID: "data_source.id", sqlID: "data_source.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read"}, actions: []string{"datasources:read"},
permissions: map[string][]string{ permissions: map[string][]string{
"datasources:read": {"datasources:id:1*"}, "datasources:read": {"datasources:id:1*"},
@@ -86,7 +86,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect error if sqlID is not in the accept list", desc: "expect error if sqlID is not in the accept list",
sqlID: "other.id", sqlID: "other.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read"}, actions: []string{"datasources:read"},
permissions: map[string][]string{ permissions: map[string][]string{
"datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8"}, "datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8"},
@@ -96,7 +96,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect data sources that users has several actions for", desc: "expect data sources that users has several actions for",
sqlID: "data_source.id", sqlID: "data_source.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read", "datasources:write"}, actions: []string{"datasources:read", "datasources:write"},
permissions: map[string][]string{ permissions: map[string][]string{
"datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8"}, "datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8"},
@@ -108,7 +108,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect data sources that users has several actions for", desc: "expect data sources that users has several actions for",
sqlID: "data_source.id", sqlID: "data_source.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read", "datasources:write"}, actions: []string{"datasources:read", "datasources:write"},
permissions: map[string][]string{ permissions: map[string][]string{
"datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8"}, "datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8"},
@@ -120,7 +120,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect no data sources when scopes does not match", desc: "expect no data sources when scopes does not match",
sqlID: "data_source.id", sqlID: "data_source.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read", "datasources:write"}, actions: []string{"datasources:read", "datasources:write"},
permissions: map[string][]string{ permissions: map[string][]string{
"datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8"}, "datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8"},
@@ -132,7 +132,7 @@ func TestFilter_Datasources(t *testing.T) {
{ {
desc: "expect to not crash if duplicates in the scope", desc: "expect to not crash if duplicates in the scope",
sqlID: "data_source.id", sqlID: "data_source.id",
prefix: "datasources", prefix: "datasources:id:",
actions: []string{"datasources:read", "datasources:write"}, actions: []string{"datasources:read", "datasources:write"},
permissions: map[string][]string{ permissions: map[string][]string{
"datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8", "datasources:id:3", "datasources:id:8"}, "datasources:read": {"datasources:id:3", "datasources:id:7", "datasources:id:8", "datasources:id:3", "datasources:id:8"},

View File

@@ -242,6 +242,7 @@ type SetResourcePermissionCommand struct {
const ( const (
GlobalOrgID = 0 GlobalOrgID = 0
// Permission actions // Permission actions
ActionAPIKeyRead = "apikeys:read" ActionAPIKeyRead = "apikeys:read"

View File

@@ -82,8 +82,9 @@ func ProvideTeamPermissions(
ac accesscontrol.AccessControl, store resourcepermissions.Store, ac accesscontrol.AccessControl, store resourcepermissions.Store,
) (*resourcepermissions.Service, error) { ) (*resourcepermissions.Service, error) {
options := resourcepermissions.Options{ options := resourcepermissions.Options{
Resource: "teams", Resource: "teams",
OnlyManaged: true, ResourceAttribute: "id",
OnlyManaged: true,
ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error {
id, err := strconv.ParseInt(resourceID, 10, 64) id, err := strconv.ParseInt(resourceID, 10, 64)
if err != nil { if err != nil {
@@ -161,7 +162,8 @@ func provideDashboardService(
} }
options := resourcepermissions.Options{ options := resourcepermissions.Options{
Resource: "dashboards", Resource: "dashboards",
ResourceAttribute: "id",
ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error {
dashboard, err := getDashboard(ctx, orgID, resourceID) dashboard, err := getDashboard(ctx, orgID, resourceID)
if err != nil { if err != nil {
@@ -217,7 +219,8 @@ func provideFolderService(
accesscontrol accesscontrol.AccessControl, store resourcepermissions.Store, accesscontrol accesscontrol.AccessControl, store resourcepermissions.Store,
) (*resourcepermissions.Service, error) { ) (*resourcepermissions.Service, error) {
options := resourcepermissions.Options{ options := resourcepermissions.Options{
Resource: "folders", Resource: "folders",
ResourceAttribute: "id",
ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error {
id, err := strconv.ParseInt(resourceID, 10, 64) id, err := strconv.ParseInt(resourceID, 10, 64)
if err != nil { if err != nil {

View File

@@ -35,13 +35,13 @@ func (a *api) registerEndpoints() {
uidSolver := solveUID(a.service.options.UidSolver) uidSolver := solveUID(a.service.options.UidSolver)
disable := middleware.Disable(a.ac.IsDisabled()) disable := middleware.Disable(a.ac.IsDisabled())
a.router.Group(fmt.Sprintf("/api/access-control/%s", a.service.options.Resource), func(r routing.RouteRegister) { a.router.Group(fmt.Sprintf("/api/access-control/%s", a.service.options.Resource), func(r routing.RouteRegister) {
idScope := accesscontrol.Scope(a.service.options.Resource, "id", accesscontrol.Parameter(":resourceID")) scope := accesscontrol.Scope(a.service.options.Resource, a.service.options.ResourceAttribute, accesscontrol.Parameter(":resourceID"))
actionWrite, actionRead := fmt.Sprintf("%s.permissions:write", a.service.options.Resource), fmt.Sprintf("%s.permissions:read", a.service.options.Resource) actionWrite, actionRead := fmt.Sprintf("%s.permissions:write", a.service.options.Resource), fmt.Sprintf("%s.permissions:read", a.service.options.Resource)
r.Get("/description", auth(disable, accesscontrol.EvalPermission(actionRead)), routing.Wrap(a.getDescription)) r.Get("/description", auth(disable, accesscontrol.EvalPermission(actionRead)), routing.Wrap(a.getDescription))
r.Get("/:resourceID", uidSolver, auth(disable, accesscontrol.EvalPermission(actionRead, idScope)), routing.Wrap(a.getPermissions)) r.Get("/:resourceID", uidSolver, auth(disable, accesscontrol.EvalPermission(actionRead, scope)), routing.Wrap(a.getPermissions))
r.Post("/:resourceID/users/:userID", uidSolver, auth(disable, accesscontrol.EvalPermission(actionWrite, idScope)), routing.Wrap(a.setUserPermission)) r.Post("/:resourceID/users/:userID", uidSolver, auth(disable, accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setUserPermission))
r.Post("/:resourceID/teams/:teamID", uidSolver, auth(disable, accesscontrol.EvalPermission(actionWrite, idScope)), routing.Wrap(a.setTeamPermission)) r.Post("/:resourceID/teams/:teamID", uidSolver, auth(disable, accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setTeamPermission))
r.Post("/:resourceID/builtInRoles/:builtInRole", uidSolver, auth(disable, accesscontrol.EvalPermission(actionWrite, idScope)), routing.Wrap(a.setBuiltinRolePermission)) r.Post("/:resourceID/builtInRoles/:builtInRole", uidSolver, auth(disable, accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setBuiltinRolePermission))
}) })
} }

View File

@@ -36,7 +36,8 @@ func TestApi_getDescription(t *testing.T) {
{ {
desc: "should return description", desc: "should return description",
options: Options{ options: Options{
Resource: "dashboards", Resource: "dashboards",
ResourceAttribute: "uid",
Assignments: Assignments{ Assignments: Assignments{
Users: true, Users: true,
Teams: true, Teams: true,
@@ -64,7 +65,8 @@ func TestApi_getDescription(t *testing.T) {
{ {
desc: "should only return user assignment", desc: "should only return user assignment",
options: Options{ options: Options{
Resource: "dashboards", Resource: "dashboards",
ResourceAttribute: "uid",
Assignments: Assignments{ Assignments: Assignments{
Users: true, Users: true,
Teams: false, Teams: false,
@@ -90,7 +92,8 @@ func TestApi_getDescription(t *testing.T) {
{ {
desc: "should return 403 when missing read permission", desc: "should return 403 when missing read permission",
options: Options{ options: Options{
Resource: "dashboards", Resource: "dashboards",
ResourceAttribute: "uid",
Assignments: Assignments{ Assignments: Assignments{
Users: true, Users: true,
Teams: false, Teams: false,
@@ -514,7 +517,8 @@ func contextProvider(tc *testContext) web.Handler {
} }
var testOptions = Options{ var testOptions = Options{
Resource: "dashboards", Resource: "dashboards",
ResourceAttribute: "id",
Assignments: Assignments{ Assignments: Assignments{
Users: true, Users: true,
Teams: true, Teams: true,

View File

@@ -14,6 +14,8 @@ type InheritedScopesSolver func(ctx context.Context, orgID int64, resourceID str
type Options struct { type Options struct {
// Resource is the action and scope prefix that is generated // Resource is the action and scope prefix that is generated
Resource string Resource string
// ResourceAttribute is the attribute the scope should be based on (e.g. id or uid)
ResourceAttribute string
// OnlyManaged will tell the service to return all permissions if set to false and only managed permissions if set to true // OnlyManaged will tell the service to return all permissions if set to false and only managed permissions if set to true
OnlyManaged bool OnlyManaged bool
// ResourceValidator is a validator function that will be called before each assignment. // ResourceValidator is a validator function that will be called before each assignment.

View File

@@ -111,12 +111,13 @@ func (s *Service) GetPermissions(ctx context.Context, user *models.SignedInUser,
} }
return s.store.GetResourcePermissions(ctx, user.OrgId, types.GetResourcePermissionsQuery{ return s.store.GetResourcePermissions(ctx, user.OrgId, types.GetResourcePermissionsQuery{
User: user, User: user,
Actions: s.actions, Actions: s.actions,
Resource: s.options.Resource, Resource: s.options.Resource,
ResourceID: resourceID, ResourceID: resourceID,
InheritedScopes: inheritedScopes, ResourceAttribute: s.options.ResourceAttribute,
OnlyManaged: s.options.OnlyManaged, InheritedScopes: inheritedScopes,
OnlyManaged: s.options.OnlyManaged,
}) })
} }
@@ -135,10 +136,11 @@ func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user acces
} }
return s.store.SetUserResourcePermission(ctx, orgID, user, types.SetResourcePermissionCommand{ return s.store.SetUserResourcePermission(ctx, orgID, user, types.SetResourcePermissionCommand{
Actions: actions, Actions: actions,
Permission: permission, Permission: permission,
ResourceID: resourceID, Resource: s.options.Resource,
Resource: s.options.Resource, ResourceID: resourceID,
ResourceAttribute: s.options.ResourceAttribute,
}, s.options.OnSetUser) }, s.options.OnSetUser)
} }
@@ -157,10 +159,11 @@ func (s *Service) SetTeamPermission(ctx context.Context, orgID, teamID int64, re
} }
return s.store.SetTeamResourcePermission(ctx, orgID, teamID, types.SetResourcePermissionCommand{ return s.store.SetTeamResourcePermission(ctx, orgID, teamID, types.SetResourcePermissionCommand{
Actions: actions, Actions: actions,
Permission: permission, Permission: permission,
ResourceID: resourceID, Resource: s.options.Resource,
Resource: s.options.Resource, ResourceID: resourceID,
ResourceAttribute: s.options.ResourceAttribute,
}, s.options.OnSetTeam) }, s.options.OnSetTeam)
} }
@@ -179,10 +182,11 @@ func (s *Service) SetBuiltInRolePermission(ctx context.Context, orgID int64, bui
} }
return s.store.SetBuiltInResourcePermission(ctx, orgID, builtInRole, types.SetResourcePermissionCommand{ return s.store.SetBuiltInResourcePermission(ctx, orgID, builtInRole, types.SetResourcePermissionCommand{
Actions: actions, Actions: actions,
Permission: permission, Permission: permission,
ResourceID: resourceID, Resource: s.options.Resource,
Resource: s.options.Resource, ResourceID: resourceID,
ResourceAttribute: s.options.ResourceAttribute,
}, s.options.OnSetBuiltInRole) }, s.options.OnSetBuiltInRole)
} }
@@ -220,10 +224,11 @@ func (s *Service) SetPermissions(
TeamID: cmd.TeamID, TeamID: cmd.TeamID,
BuiltinRole: cmd.BuiltinRole, BuiltinRole: cmd.BuiltinRole,
SetResourcePermissionCommand: types.SetResourcePermissionCommand{ SetResourcePermissionCommand: types.SetResourcePermissionCommand{
Actions: actions, Actions: actions,
Resource: s.options.Resource, Resource: s.options.Resource,
ResourceID: resourceID, ResourceID: resourceID,
Permission: cmd.Permission, ResourceAttribute: s.options.ResourceAttribute,
Permission: cmd.Permission,
}, },
}) })
} }
@@ -301,7 +306,7 @@ func (s *Service) declareFixedRoles() error {
scopeAll := accesscontrol.Scope(s.options.Resource, "*") scopeAll := accesscontrol.Scope(s.options.Resource, "*")
readerRole := accesscontrol.RoleRegistration{ readerRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{ Role: accesscontrol.RoleDTO{
Version: 5, Version: 6,
Name: fmt.Sprintf("fixed:%s.permissions:reader", s.options.Resource), Name: fmt.Sprintf("fixed:%s.permissions:reader", s.options.Resource),
DisplayName: s.options.ReaderRoleName, DisplayName: s.options.ReaderRoleName,
Group: s.options.RoleGroup, Group: s.options.RoleGroup,
@@ -314,7 +319,7 @@ func (s *Service) declareFixedRoles() error {
writerRole := accesscontrol.RoleRegistration{ writerRole := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{ Role: accesscontrol.RoleDTO{
Version: 5, Version: 6,
Name: fmt.Sprintf("fixed:%s.permissions:writer", s.options.Resource), Name: fmt.Sprintf("fixed:%s.permissions:writer", s.options.Resource),
DisplayName: s.options.WriterRoleName, DisplayName: s.options.WriterRoleName,
Group: s.options.RoleGroup, Group: s.options.RoleGroup,

View File

@@ -6,10 +6,11 @@ import (
) )
type SetResourcePermissionCommand struct { type SetResourcePermissionCommand struct {
Actions []string Actions []string
Resource string Resource string
ResourceID string ResourceID string
Permission string ResourceAttribute string
Permission string
} }
type SetResourcePermissionsCommand struct { type SetResourcePermissionsCommand struct {
@@ -21,10 +22,11 @@ type SetResourcePermissionsCommand struct {
} }
type GetResourcePermissionsQuery struct { type GetResourcePermissionsQuery struct {
Actions []string Actions []string
Resource string Resource string
ResourceID string ResourceID string
OnlyManaged bool ResourceAttribute string
InheritedScopes []string OnlyManaged bool
User *models.SignedInUser InheritedScopes []string
User *models.SignedInUser
} }

View File

@@ -147,7 +147,7 @@ func (api *ServiceAccountsAPI) getAccessControlMetadata(c *models.ReqContext, sa
return map[string]accesscontrol.Metadata{} return map[string]accesscontrol.Metadata{}
} }
return accesscontrol.GetResourcesMetadata(c.Req.Context(), permissions, "serviceaccounts", saIDs) return accesscontrol.GetResourcesMetadata(c.Req.Context(), permissions, "serviceaccounts:id:", saIDs)
} }
func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) response.Response { func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) response.Response {

View File

@@ -314,7 +314,7 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(
s.sqlStore.Dialect.BooleanStr(true))) s.sqlStore.Dialect.BooleanStr(true)))
if s.sqlStore.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { if s.sqlStore.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
acFilter, err := accesscontrol.Filter(signedInUser, "org_user.user_id", "serviceaccounts", serviceaccounts.ActionRead) acFilter, err := accesscontrol.Filter(signedInUser, "org_user.user_id", "serviceaccounts:id:", serviceaccounts.ActionRead)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -118,7 +118,7 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu
whereParams = append(whereParams, dialect.BooleanStr(false)) whereParams = append(whereParams, dialect.BooleanStr(false))
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) && query.User != nil { if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) && query.User != nil {
acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "users", accesscontrol.ActionOrgUsersRead) acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "users:id:", accesscontrol.ActionOrgUsersRead)
if err != nil { if err != nil {
return err return err
} }
@@ -181,7 +181,7 @@ func (ss *SQLStore) SearchOrgUsers(ctx context.Context, query *models.SearchOrgU
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %s", x.Dialect().Quote("user"), ss.Dialect.BooleanStr(false))) whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %s", x.Dialect().Quote("user"), ss.Dialect.BooleanStr(false)))
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "users", accesscontrol.ActionOrgUsersRead) acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "users:id:", accesscontrol.ActionOrgUsersRead)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -110,13 +110,13 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
if len(f.dashboardActions) > 0 { if len(f.dashboardActions) > 0 {
builder.WriteString("((") builder.WriteString("((")
dashFilter, _ := accesscontrol.Filter(f.User, "dashboard.id", "dashboards", f.dashboardActions...) dashFilter, _ := accesscontrol.Filter(f.User, "dashboard.id", "dashboards:id:", f.dashboardActions...)
builder.WriteString(dashFilter.Where) builder.WriteString(dashFilter.Where)
args = append(args, dashFilter.Args...) args = append(args, dashFilter.Args...)
builder.WriteString(" OR ") builder.WriteString(" OR ")
dashFolderFilter, _ := accesscontrol.Filter(f.User, "dashboard.folder_id", "folders", f.dashboardActions...) dashFolderFilter, _ := accesscontrol.Filter(f.User, "dashboard.folder_id", "folders:id:", f.dashboardActions...)
builder.WriteString(dashFolderFilter.Where) builder.WriteString(dashFolderFilter.Where)
builder.WriteString(") AND NOT dashboard.is_folder)") builder.WriteString(") AND NOT dashboard.is_folder)")
args = append(args, dashFolderFilter.Args...) args = append(args, dashFolderFilter.Args...)
@@ -127,7 +127,7 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
builder.WriteString(" OR ") builder.WriteString(" OR ")
} }
builder.WriteString("(") builder.WriteString("(")
folderFilter, _ := accesscontrol.Filter(f.User, "dashboard.id", "folders", f.folderActions...) folderFilter, _ := accesscontrol.Filter(f.User, "dashboard.id", "folders:id:", f.folderActions...)
builder.WriteString(folderFilter.Where) builder.WriteString(folderFilter.Where)
builder.WriteString(" AND dashboard.is_folder)") builder.WriteString(" AND dashboard.is_folder)")
args = append(args, folderFilter.Args...) args = append(args, folderFilter.Args...)

View File

@@ -229,7 +229,7 @@ func (ss *SQLStore) SearchTeams(ctx context.Context, query *models.SearchTeamsQu
err error err error
) )
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
acFilter, err = ac.Filter(query.SignedInUser, "team.id", "teams", ac.ActionTeamsRead) acFilter, err = ac.Filter(query.SignedInUser, "team.id", "teams:id:", ac.ActionTeamsRead)
if err != nil { if err != nil {
return err return err
} }
@@ -529,7 +529,7 @@ func (ss *SQLStore) GetTeamMembers(ctx context.Context, query *models.GetTeamMem
// If the signed in user is not set no member will be returned // If the signed in user is not set no member will be returned
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
sqlID := fmt.Sprintf("%s.%s", x.Dialect().Quote("user"), x.Dialect().Quote("id")) sqlID := fmt.Sprintf("%s.%s", x.Dialect().Quote("user"), x.Dialect().Quote("id"))
*acFilter, err = ac.Filter(query.SignedInUser, sqlID, "users", ac.ActionOrgUsersRead) *acFilter, err = ac.Filter(query.SignedInUser, sqlID, "users:id:", ac.ActionOrgUsersRead)
if err != nil { if err != nil {
return err return err
} }