mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
RBAC: Add a function to save external service roles (#66299)
* AuthN: Save external services RBAC roles * Add missing test * Placing roles in the same group * Split function to gen role and assignment * add test case and comments * Ensure we check external service roles are assigned once only * Update pkg/services/accesscontrol/models_test.go Co-authored-by: Misi <mgyongyosi@users.noreply.github.com> --------- Co-authored-by: Misi <mgyongyosi@users.noreply.github.com>
This commit is contained in:
parent
04df92ab47
commit
8c6b5a4319
@ -38,6 +38,8 @@ type Service interface {
|
||||
// DeclareFixedRoles allows the caller to declare, to the service, fixed roles and their
|
||||
// assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
|
||||
DeclareFixedRoles(registrations ...RoleRegistration) error
|
||||
// SaveExternalServiceRole creates or updates an external service's role and assigns it to a given service account id.
|
||||
SaveExternalServiceRole(ctx context.Context, cmd SaveExternalServiceRoleCommand) error
|
||||
//IsDisabled returns if access control is enabled or not
|
||||
IsDisabled() bool
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ type store interface {
|
||||
SearchUsersPermissions(ctx context.Context, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error)
|
||||
GetUsersBasicRoles(ctx context.Context, userFilter []int64, orgID int64) (map[int64][]string, error)
|
||||
DeleteUserPermissions(ctx context.Context, orgID, userID int64) error
|
||||
SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error
|
||||
}
|
||||
|
||||
// Service is the service implementing role based access control.
|
||||
@ -107,11 +108,11 @@ func (s *Service) getUserPermissions(ctx context.Context, user *user.SignedInUse
|
||||
}
|
||||
|
||||
dbPermissions, err := s.store.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
|
||||
OrgID: user.OrgID,
|
||||
UserID: user.UserID,
|
||||
Roles: accesscontrol.GetOrgRoles(user),
|
||||
TeamIDs: user.Teams,
|
||||
RolePrefix: accesscontrol.ManagedRolePrefix,
|
||||
OrgID: user.OrgID,
|
||||
UserID: user.UserID,
|
||||
Roles: accesscontrol.GetOrgRoles(user),
|
||||
TeamIDs: user.Teams,
|
||||
RolePrefixes: []string{accesscontrol.ManagedRolePrefix, accesscontrol.ExternalServiceRolePrefix},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -414,3 +415,21 @@ func PermissionMatchesSearchOptions(permission accesscontrol.Permission, searchO
|
||||
}
|
||||
return strings.HasPrefix(permission.Action, searchOptions.ActionPrefix)
|
||||
}
|
||||
|
||||
func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error {
|
||||
// If accesscontrol is disabled no need to save the external service role
|
||||
if accesscontrol.IsDisabled(s.cfg) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !s.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
|
||||
s.log.Debug("registering external service role is behind a feature flag, enable it to use this feature.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := cmd.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.store.SaveExternalServiceRole(ctx, cmd)
|
||||
}
|
||||
|
@ -740,3 +740,88 @@ func TestPermissionCacheKey(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_SaveExternalServiceRole(t *testing.T) {
|
||||
type run struct {
|
||||
cmd accesscontrol.SaveExternalServiceRoleCommand
|
||||
wantErr bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
runs []run
|
||||
}{
|
||||
{
|
||||
name: "can create a role",
|
||||
runs: []run{
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
OrgID: 2,
|
||||
ServiceAccountID: 2,
|
||||
ExternalServiceID: "App 1",
|
||||
Permissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:id:1"}},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "can update a role",
|
||||
runs: []run{
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
Global: true,
|
||||
ServiceAccountID: 2,
|
||||
ExternalServiceID: "App 1",
|
||||
Permissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:id:1"}},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
Global: true,
|
||||
ServiceAccountID: 2,
|
||||
ExternalServiceID: "App 1",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "users:write", Scope: "users:id:1"},
|
||||
{Action: "users:write", Scope: "users:id:2"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test command validity - no service account ID",
|
||||
runs: []run{
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
OrgID: 2,
|
||||
ExternalServiceID: "App 1",
|
||||
Permissions: []accesscontrol.Permission{{Action: "users:read", Scope: "users:id:1"}},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ac := setupTestEnv(t)
|
||||
ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth)
|
||||
for _, r := range tt.runs {
|
||||
err := ac.SaveExternalServiceRole(ctx, r.cmd)
|
||||
if r.wantErr {
|
||||
require.Error(t, err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that the permissions and assignment are stored correctly
|
||||
perms, errGetPerms := ac.getUserPermissions(ctx, &user.SignedInUser{OrgID: r.cmd.OrgID, UserID: 2}, accesscontrol.Options{})
|
||||
require.NoError(t, errGetPerms)
|
||||
assert.ElementsMatch(t, r.cmd.Permissions, perms)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,10 @@ func (f FakeService) IsDisabled() bool {
|
||||
return f.ExpectedDisabled
|
||||
}
|
||||
|
||||
func (f FakeService) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error {
|
||||
return f.ExpectedErr
|
||||
}
|
||||
|
||||
var _ accesscontrol.AccessControl = new(FakeAccessControl)
|
||||
|
||||
type FakeAccessControl struct {
|
||||
@ -95,6 +99,10 @@ func (f FakeStore) DeleteUserPermissions(ctx context.Context, orgID, userID int6
|
||||
return f.ExpectedErr
|
||||
}
|
||||
|
||||
func (f FakeStore) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error {
|
||||
return f.ExpectedErr
|
||||
}
|
||||
|
||||
var _ accesscontrol.PermissionsService = new(FakePermissionsService)
|
||||
|
||||
type FakePermissionsService struct {
|
||||
|
@ -35,9 +35,12 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces
|
||||
INNER JOIN role ON role.id = permission.role_id
|
||||
` + filter
|
||||
|
||||
if query.RolePrefix != "" {
|
||||
q += " WHERE role.name LIKE ?"
|
||||
params = append(params, query.RolePrefix+"%")
|
||||
if len(query.RolePrefixes) > 0 {
|
||||
q += " WHERE ( " + strings.Repeat("role.name LIKE ? OR ", len(query.RolePrefixes))
|
||||
q = q[:len(q)-4] + " )" // remove last " OR "
|
||||
for i := range query.RolePrefixes {
|
||||
params = append(params, query.RolePrefixes[i]+"%")
|
||||
}
|
||||
}
|
||||
|
||||
if err := sess.SQL(q, params...).Find(&result); err != nil {
|
||||
|
198
pkg/services/accesscontrol/database/externalservices.go
Normal file
198
pkg/services/accesscontrol/database/externalservices.go
Normal file
@ -0,0 +1,198 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
func (s *AccessControlStore) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error {
|
||||
role := genExternalServiceRole(cmd)
|
||||
assignment := genExternalServiceAssignment(cmd)
|
||||
|
||||
return s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
// Create or update the role
|
||||
existingRole, errSaveRole := s.saveRole(ctx, sess, &role)
|
||||
if errSaveRole != nil {
|
||||
return errSaveRole
|
||||
}
|
||||
// Assign role to service account
|
||||
// We update the assignment before the permissions to avoid an edge case.
|
||||
// If the role is assigned to another service account (which can only happen if the services have the same ID)
|
||||
// and permissions are updated before the assignment, this would result in the other service account acquiring
|
||||
// a different set of permissions.
|
||||
assignment.RoleID = existingRole.ID
|
||||
errSaveAssign := s.saveUserAssignment(ctx, sess, assignment)
|
||||
if errSaveAssign != nil {
|
||||
return errSaveAssign
|
||||
}
|
||||
// Update permissions
|
||||
return s.savePermissions(ctx, sess, existingRole.ID, cmd.Permissions)
|
||||
})
|
||||
}
|
||||
|
||||
func genExternalServiceRole(cmd accesscontrol.SaveExternalServiceRoleCommand) accesscontrol.Role {
|
||||
role := accesscontrol.Role{
|
||||
OrgID: cmd.OrgID,
|
||||
Version: 1,
|
||||
Name: fmt.Sprintf("%s%s:permissions", accesscontrol.ExternalServiceRolePrefix, cmd.ExternalServiceID),
|
||||
UID: fmt.Sprintf("%s%s_permissions", accesscontrol.ExternalServiceRoleUIDPrefix, cmd.ExternalServiceID),
|
||||
DisplayName: fmt.Sprintf("External Service %s Permissions", cmd.ExternalServiceID),
|
||||
Description: fmt.Sprintf("External Service %s permissions", cmd.ExternalServiceID),
|
||||
Group: "External Service",
|
||||
Hidden: true,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
if cmd.Global {
|
||||
role.OrgID = accesscontrol.GlobalOrgID
|
||||
}
|
||||
return role
|
||||
}
|
||||
|
||||
func genExternalServiceAssignment(cmd accesscontrol.SaveExternalServiceRoleCommand) accesscontrol.UserRole {
|
||||
assignment := accesscontrol.UserRole{
|
||||
OrgID: cmd.OrgID,
|
||||
UserID: cmd.ServiceAccountID,
|
||||
Created: time.Now(),
|
||||
}
|
||||
if cmd.Global {
|
||||
assignment.OrgID = accesscontrol.GlobalOrgID
|
||||
}
|
||||
return assignment
|
||||
}
|
||||
|
||||
func getRoleByUID(ctx context.Context, sess *db.Session, uid string) (*accesscontrol.Role, error) {
|
||||
var role accesscontrol.Role
|
||||
has, err := sess.Where("uid = ?", uid).Get(&role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, accesscontrol.ErrRoleNotFound
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
func getRoleAssignments(ctx context.Context, sess *db.Session, roleID int64) ([]accesscontrol.UserRole, error) {
|
||||
var assignements []accesscontrol.UserRole
|
||||
if err := sess.Where("role_id = ?", roleID).Find(&assignements); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return assignements, nil
|
||||
}
|
||||
|
||||
func getRolePermissions(ctx context.Context, sess *db.Session, id int64) ([]accesscontrol.Permission, error) {
|
||||
var permissions []accesscontrol.Permission
|
||||
if err := sess.Where("role_id = ?", id).Find(&permissions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
func permissionDiff(previous, new []accesscontrol.Permission) (added, removed []accesscontrol.Permission) {
|
||||
type key struct{ Action, Scope string }
|
||||
prevMap := map[key]int64{}
|
||||
for i := range previous {
|
||||
prevMap[key{previous[i].Action, previous[i].Scope}] = previous[i].ID
|
||||
}
|
||||
newMap := map[key]int64{}
|
||||
for i := range new {
|
||||
newMap[key{new[i].Action, new[i].Scope}] = 0
|
||||
}
|
||||
for i := range new {
|
||||
key := key{new[i].Action, new[i].Scope}
|
||||
if _, already := prevMap[key]; !already {
|
||||
added = append(added, new[i])
|
||||
} else {
|
||||
delete(prevMap, key)
|
||||
}
|
||||
}
|
||||
|
||||
for p, id := range prevMap {
|
||||
removed = append(removed, accesscontrol.Permission{ID: id, Action: p.Action, Scope: p.Scope})
|
||||
}
|
||||
|
||||
return added, removed
|
||||
}
|
||||
|
||||
func (*AccessControlStore) saveRole(ctx context.Context, sess *db.Session, role *accesscontrol.Role) (*accesscontrol.Role, error) {
|
||||
existingRole, err := getRoleByUID(ctx, sess, role.UID)
|
||||
if err != nil && !errors.Is(err, accesscontrol.ErrRoleNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existingRole == nil {
|
||||
if _, err := sess.Insert(role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
role.ID = existingRole.ID
|
||||
role.Created = existingRole.Created
|
||||
if _, err := sess.Where("id = ?", existingRole.ID).MustCols("org_id").Update(role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return getRoleByUID(ctx, sess, role.UID)
|
||||
}
|
||||
|
||||
func (*AccessControlStore) savePermissions(ctx context.Context, sess *db.Session, roleID int64, permissions []accesscontrol.Permission) error {
|
||||
now := time.Now()
|
||||
storedPermissions, err := getRolePermissions(ctx, sess, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
added, removed := permissionDiff(storedPermissions, permissions)
|
||||
if len(added) > 0 {
|
||||
for i := range added {
|
||||
added[i].RoleID = roleID
|
||||
added[i].Created = now
|
||||
added[i].Updated = now
|
||||
}
|
||||
if _, err := sess.Insert(&added); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(removed) > 0 {
|
||||
ids := make([]int64, len(removed))
|
||||
for i := range removed {
|
||||
ids[i] = removed[i].ID
|
||||
}
|
||||
count, err := sess.In("id", ids).Delete(&accesscontrol.Permission{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count != int64(len(removed)) {
|
||||
return errors.New("failed to delete permissions that have been removed from role")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*AccessControlStore) saveUserAssignment(ctx context.Context, sess *db.Session, assignment accesscontrol.UserRole) error {
|
||||
// alreadyAssigned checks if the assignment already exists without accounting for the organization
|
||||
assignments, errGetAssigns := getRoleAssignments(ctx, sess, assignment.RoleID)
|
||||
if errGetAssigns != nil {
|
||||
return errGetAssigns
|
||||
}
|
||||
|
||||
if len(assignments) == 0 {
|
||||
if _, errInsert := sess.Insert(&assignment); errInsert != nil {
|
||||
return errInsert
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure the role was assigned only to this service account
|
||||
if len(assignments) > 1 || assignments[0].UserID != assignment.UserID {
|
||||
return errors.New("external service role assigned to another user or service account")
|
||||
}
|
||||
|
||||
// Ensure the assignment is in the correct organization
|
||||
_, errUpdate := sess.Where("role_id = ? AND user_id = ?", assignment.RoleID, assignment.UserID).MustCols("org_id").Update(&assignment)
|
||||
return errUpdate
|
||||
}
|
191
pkg/services/accesscontrol/database/externalservices_test.go
Normal file
191
pkg/services/accesscontrol/database/externalservices_test.go
Normal file
@ -0,0 +1,191 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAccessControlStore_SaveExternalServiceRole(t *testing.T) {
|
||||
type run struct {
|
||||
cmd accesscontrol.SaveExternalServiceRoleCommand
|
||||
wantErr bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
runs []run
|
||||
}{
|
||||
{
|
||||
name: "create app role",
|
||||
runs: []run{
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "app1",
|
||||
Global: true,
|
||||
ServiceAccountID: 1,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "users:read", Scope: "users:id:1"},
|
||||
{Action: "users:read", Scope: "users:id:2"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update app role",
|
||||
runs: []run{
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "app1",
|
||||
Global: true,
|
||||
ServiceAccountID: 1,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "users:read", Scope: "users:id:1"},
|
||||
{Action: "users:read", Scope: "users:id:2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "app1",
|
||||
Global: true,
|
||||
ServiceAccountID: 1,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "users:write", Scope: "users:id:1"},
|
||||
{Action: "users:write", Scope: "users:id:2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow switching role from local to global and back",
|
||||
runs: []run{
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "app1",
|
||||
OrgID: 1,
|
||||
ServiceAccountID: 1,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "users:read", Scope: "users:id:1"},
|
||||
{Action: "users:read", Scope: "users:id:2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "app1",
|
||||
Global: true,
|
||||
ServiceAccountID: 1,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "users:read", Scope: "users:id:1"},
|
||||
{Action: "users:read", Scope: "users:id:2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "app1",
|
||||
OrgID: 1,
|
||||
ServiceAccountID: 1,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "users:read", Scope: "users:id:1"},
|
||||
{Action: "users:read", Scope: "users:id:2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edge case - remove all permissions",
|
||||
runs: []run{
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "app1",
|
||||
Global: true,
|
||||
ServiceAccountID: 1,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "users:read", Scope: "users:id:1"},
|
||||
{Action: "users:read", Scope: "users:id:2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "app1",
|
||||
Global: true,
|
||||
ServiceAccountID: 1,
|
||||
Permissions: []accesscontrol.Permission{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "edge case - reassign to another service account",
|
||||
runs: []run{
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "app1",
|
||||
Global: true,
|
||||
ServiceAccountID: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
cmd: accesscontrol.SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "app1",
|
||||
Global: true,
|
||||
ServiceAccountID: 2,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := &AccessControlStore{
|
||||
sql: db.InitTestDB(t),
|
||||
}
|
||||
|
||||
for i := range tt.runs {
|
||||
err := s.SaveExternalServiceRole(ctx, tt.runs[i].cmd)
|
||||
if tt.runs[i].wantErr {
|
||||
require.Error(t, err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
errDBSession := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
storedRole, err := getRoleByUID(ctx, sess, fmt.Sprintf("externalservice_%s_permissions", tt.runs[i].cmd.ExternalServiceID))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, storedRole)
|
||||
require.Equal(t, tt.runs[i].cmd.Global, storedRole.Global(), "Incorrect global state of the role")
|
||||
require.Equal(t, tt.runs[i].cmd.OrgID, storedRole.OrgID, "Incorrect OrgID of the role")
|
||||
|
||||
storedPerm, err := getRolePermissions(ctx, sess, storedRole.ID)
|
||||
require.NoError(t, err)
|
||||
for i := range storedPerm {
|
||||
storedPerm[i] = accesscontrol.Permission{Action: storedPerm[i].Action, Scope: storedPerm[i].Scope}
|
||||
}
|
||||
require.ElementsMatch(t, tt.runs[i].cmd.Permissions, storedPerm)
|
||||
|
||||
var assignment accesscontrol.UserRole
|
||||
has, err := sess.Where("role_id = ? AND user_id = ?", storedRole.ID, tt.runs[i].cmd.ServiceAccountID).Get(&assignment)
|
||||
require.NoError(t, err)
|
||||
require.True(t, has)
|
||||
require.Equal(t, tt.runs[i].cmd.Global, assignment.OrgID == accesscontrol.GlobalOrgID, "Incorrect global state of the assignment")
|
||||
require.Equal(t, tt.runs[i].cmd.OrgID, assignment.OrgID, "Incorrect OrgID for the role assignment")
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, errDBSession)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ var (
|
||||
ErrInvalidScope = errors.New("invalid scope")
|
||||
ErrResolverNotFound = errors.New("no resolver found")
|
||||
ErrPluginIDRequired = errors.New("plugin ID is required")
|
||||
ErrRoleNotFound = errors.New("role not found")
|
||||
)
|
||||
|
||||
type ErrorInvalidRole struct{}
|
||||
|
@ -30,6 +30,7 @@ type Calls struct {
|
||||
DeleteUserPermissions []interface{}
|
||||
SearchUsersPermissions []interface{}
|
||||
SearchUserPermissions []interface{}
|
||||
SaveExternalServiceRole []interface{}
|
||||
}
|
||||
|
||||
type Mock struct {
|
||||
@ -56,6 +57,7 @@ type Mock struct {
|
||||
DeleteUserPermissionsFunc func(context.Context, int64) error
|
||||
SearchUsersPermissionsFunc func(context.Context, *user.SignedInUser, int64, accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error)
|
||||
SearchUserPermissionsFunc func(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error)
|
||||
SaveExternalServiceRoleFunc func(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error
|
||||
|
||||
scopeResolvers accesscontrol.Resolvers
|
||||
}
|
||||
@ -235,3 +237,12 @@ func (m *Mock) SearchUserPermissions(ctx context.Context, orgID int64, searchOpt
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *Mock) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error {
|
||||
m.Calls.SaveExternalServiceRole = append(m.Calls.SaveExternalServiceRole, []interface{}{ctx, cmd})
|
||||
// Use override if provided
|
||||
if m.SaveExternalServiceRoleFunc != nil {
|
||||
return m.SaveExternalServiceRoleFunc(ctx, cmd)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -2,10 +2,12 @@ package accesscontrol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
@ -129,6 +131,10 @@ func (r *RoleDTO) IsBasic() bool {
|
||||
return strings.HasPrefix(r.Name, BasicRolePrefix) || strings.HasPrefix(r.UID, BasicRoleUIDPrefix)
|
||||
}
|
||||
|
||||
func (r *RoleDTO) IsExternalService() bool {
|
||||
return strings.HasPrefix(r.Name, ExternalServiceRolePrefix) || strings.HasPrefix(r.UID, ExternalServiceRoleUIDPrefix)
|
||||
}
|
||||
|
||||
func (r RoleDTO) MarshalJSON() ([]byte, error) {
|
||||
type Alias RoleDTO
|
||||
|
||||
@ -188,11 +194,11 @@ func (p Permission) OSSPermission() Permission {
|
||||
}
|
||||
|
||||
type GetUserPermissionsQuery struct {
|
||||
OrgID int64
|
||||
UserID int64
|
||||
Roles []string
|
||||
TeamIDs []int64
|
||||
RolePrefix string
|
||||
OrgID int64
|
||||
UserID int64
|
||||
Roles []string
|
||||
TeamIDs []int64
|
||||
RolePrefixes []string
|
||||
}
|
||||
|
||||
// ResourcePermission is structure that holds all actions that either a team / user / builtin-role
|
||||
@ -245,14 +251,47 @@ type SetResourcePermissionCommand struct {
|
||||
Permission string `json:"permission"`
|
||||
}
|
||||
|
||||
type SaveExternalServiceRoleCommand struct {
|
||||
OrgID int64
|
||||
Global bool
|
||||
ExternalServiceID string
|
||||
ServiceAccountID int64
|
||||
Permissions []Permission
|
||||
}
|
||||
|
||||
func (cmd *SaveExternalServiceRoleCommand) Validate() error {
|
||||
if cmd.ExternalServiceID == "" {
|
||||
return errors.New("external service id not specified")
|
||||
}
|
||||
|
||||
// slugify the external service id ID for the role to have correct name and uid
|
||||
cmd.ExternalServiceID = slugify.Slugify(cmd.ExternalServiceID)
|
||||
|
||||
if (cmd.OrgID == GlobalOrgID) != cmd.Global {
|
||||
return fmt.Errorf("invalid org id %d for global role %t", cmd.OrgID, cmd.Global)
|
||||
}
|
||||
|
||||
if cmd.Permissions == nil || len(cmd.Permissions) == 0 {
|
||||
return errors.New("no permissions provided")
|
||||
}
|
||||
|
||||
if cmd.ServiceAccountID <= 0 {
|
||||
return fmt.Errorf("invalid service account id %d", cmd.ServiceAccountID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
GlobalOrgID = 0
|
||||
FixedRolePrefix = "fixed:"
|
||||
ManagedRolePrefix = "managed:"
|
||||
BasicRolePrefix = "basic:"
|
||||
PluginRolePrefix = "plugins:"
|
||||
BasicRoleUIDPrefix = "basic_"
|
||||
RoleGrafanaAdmin = "Grafana Admin"
|
||||
GlobalOrgID = 0
|
||||
FixedRolePrefix = "fixed:"
|
||||
ManagedRolePrefix = "managed:"
|
||||
BasicRolePrefix = "basic:"
|
||||
PluginRolePrefix = "plugins:"
|
||||
ExternalServiceRolePrefix = "externalservice:"
|
||||
BasicRoleUIDPrefix = "basic_"
|
||||
ExternalServiceRoleUIDPrefix = "externalservice_"
|
||||
RoleGrafanaAdmin = "Grafana Admin"
|
||||
|
||||
GeneralFolderUID = "general"
|
||||
|
||||
|
80
pkg/services/accesscontrol/models_test.go
Normal file
80
pkg/services/accesscontrol/models_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSaveExternalServiceRoleCommand_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd SaveExternalServiceRoleCommand
|
||||
wantID string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "invalid global statement",
|
||||
cmd: SaveExternalServiceRoleCommand{
|
||||
OrgID: 1,
|
||||
Global: true,
|
||||
ExternalServiceID: "app 1",
|
||||
ServiceAccountID: 2,
|
||||
Permissions: []Permission{{Action: "users:read", Scope: "users:id:1"}},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid no permissions",
|
||||
cmd: SaveExternalServiceRoleCommand{
|
||||
OrgID: 1,
|
||||
ExternalServiceID: "app 1",
|
||||
ServiceAccountID: 2,
|
||||
Permissions: []Permission{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid service account id",
|
||||
cmd: SaveExternalServiceRoleCommand{
|
||||
OrgID: 1,
|
||||
ExternalServiceID: "app 1",
|
||||
ServiceAccountID: -1,
|
||||
Permissions: []Permission{{Action: "users:read", Scope: "users:id:1"}},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid no Ext Service ID",
|
||||
cmd: SaveExternalServiceRoleCommand{
|
||||
OrgID: 1,
|
||||
ServiceAccountID: 2,
|
||||
Permissions: []Permission{{Action: "users:read", Scope: "users:id:1"}},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "slugify the external service ID correctly",
|
||||
cmd: SaveExternalServiceRoleCommand{
|
||||
ExternalServiceID: "ThisIs a Very Strange ___ App Name?",
|
||||
Global: true,
|
||||
ServiceAccountID: 2,
|
||||
Permissions: []Permission{{Action: "users:read", Scope: "users:id:1"}},
|
||||
},
|
||||
wantErr: false,
|
||||
wantID: "thisis-a-very-strange-app-name",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.cmd.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tt.wantID, tt.cmd.ExternalServiceID)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user