AuthN: Ext JWT support actions (#92486)
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -21,18 +22,20 @@ var (
|
||||
errSyncPermissionsForbidden = errutil.Forbidden("permissions.sync.forbidden")
|
||||
)
|
||||
|
||||
func ProvideRBACSync(acService accesscontrol.Service, tracer tracing.Tracer) *RBACSync {
|
||||
func ProvideRBACSync(acService accesscontrol.Service, tracer tracing.Tracer, permRegistry permreg.PermissionRegistry) *RBACSync {
|
||||
return &RBACSync{
|
||||
ac: acService,
|
||||
log: log.New("permissions.sync"),
|
||||
tracer: tracer,
|
||||
ac: acService,
|
||||
log: log.New("permissions.sync"),
|
||||
permRegistry: permRegistry,
|
||||
tracer: tracer,
|
||||
}
|
||||
}
|
||||
|
||||
type RBACSync struct {
|
||||
ac accesscontrol.Service
|
||||
log log.Logger
|
||||
tracer tracing.Tracer
|
||||
ac accesscontrol.Service
|
||||
permRegistry permreg.PermissionRegistry
|
||||
log log.Logger
|
||||
tracer tracing.Tracer
|
||||
}
|
||||
|
||||
func (s *RBACSync) SyncPermissionsHook(ctx context.Context, ident *authn.Identity, _ *authn.Request) error {
|
||||
@@ -56,7 +59,7 @@ func (s *RBACSync) SyncPermissionsHook(ctx context.Context, ident *authn.Identit
|
||||
grouped := accesscontrol.GroupScopesByActionContext(ctx, permissions)
|
||||
|
||||
// Restrict access to the list of actions
|
||||
actionsLookup := ident.ClientParams.FetchPermissionsParams.ActionsLookup
|
||||
actionsLookup := ident.ClientParams.FetchPermissionsParams.RestrictedActions
|
||||
if len(actionsLookup) > 0 {
|
||||
filtered := make(map[string][]string, len(actionsLookup))
|
||||
for _, action := range actionsLookup {
|
||||
@@ -77,7 +80,8 @@ func (s *RBACSync) fetchPermissions(ctx context.Context, ident *authn.Identity)
|
||||
|
||||
permissions := make([]accesscontrol.Permission, 0, 8)
|
||||
roles := ident.ClientParams.FetchPermissionsParams.Roles
|
||||
if len(roles) > 0 {
|
||||
actions := ident.ClientParams.FetchPermissionsParams.AllowedActions
|
||||
if len(roles) > 0 || len(actions) > 0 {
|
||||
for _, role := range roles {
|
||||
roleDTO, err := s.ac.GetRoleByName(ctx, ident.GetOrgID(), role)
|
||||
if err != nil && !errors.Is(err, accesscontrol.ErrRoleNotFound) {
|
||||
@@ -88,7 +92,20 @@ func (s *RBACSync) fetchPermissions(ctx context.Context, ident *authn.Identity)
|
||||
permissions = append(permissions, roleDTO.Permissions...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, action := range actions {
|
||||
scopes, ok := s.permRegistry.GetScopePrefixes(action)
|
||||
if !ok {
|
||||
s.log.Warn("Unknown action scopes", "action", action)
|
||||
continue
|
||||
}
|
||||
if len(scopes) == 0 {
|
||||
permissions = append(permissions, accesscontrol.Permission{Action: action})
|
||||
continue
|
||||
}
|
||||
for scope := range scopes {
|
||||
permissions = append(permissions, accesscontrol.Permission{Action: action, Scope: scope + "*"})
|
||||
}
|
||||
}
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
permreg "github.com/grafana/grafana/pkg/services/accesscontrol/permreg/test"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -22,34 +23,155 @@ func TestRBACSync_SyncPermission(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
identity *authn.Identity
|
||||
expectedPermissions []accesscontrol.Permission
|
||||
expectedPermissions map[string][]string
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "enriches the identity successfully when SyncPermissions is true",
|
||||
identity: &authn.Identity{ID: "2", Type: claims.TypeUser, OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}},
|
||||
expectedPermissions: []accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionUsersRead},
|
||||
expectedPermissions: map[string][]string{
|
||||
accesscontrol.ActionUsersRead: {accesscontrol.ScopeUsersAll},
|
||||
accesscontrol.ActionUsersWrite: {accesscontrol.ScopeUsersAll},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not load the permissions when SyncPermissions is false",
|
||||
identity: &authn.Identity{ID: "2", Type: claims.TypeUser, OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}},
|
||||
expectedPermissions: []accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionUsersRead},
|
||||
name: "does not load the permissions when SyncPermissions is false",
|
||||
identity: &authn.Identity{ID: "2", Type: claims.TypeUser, OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: false}},
|
||||
expectedPermissions: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := setupTestEnv(t)
|
||||
|
||||
err := s.SyncPermissionsHook(context.Background(), tt.identity, &authn.Request{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(tt.expectedPermissions), len(tt.identity.Permissions[tt.identity.OrgID]))
|
||||
for action, scopes := range tt.expectedPermissions {
|
||||
require.ElementsMatch(t, scopes, tt.identity.Permissions[tt.identity.OrgID][action])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRBACSync_FetchPermissions(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
identity *authn.Identity
|
||||
expectedPermissions map[string][]string
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "restrict permissions from store",
|
||||
identity: &authn.Identity{
|
||||
ID: "2", Type: claims.TypeUser, OrgID: 1,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
RestrictedActions: []string{accesscontrol.ActionUsersRead},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPermissions: map[string][]string{accesscontrol.ActionUsersRead: {accesscontrol.ScopeUsersAll}},
|
||||
},
|
||||
{
|
||||
name: "fetch roles permissions",
|
||||
identity: &authn.Identity{
|
||||
ID: "2", Type: claims.TypeUser, OrgID: 1,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
Roles: []string{"fixed:teams:reader"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPermissions: map[string][]string{accesscontrol.ActionTeamsRead: {accesscontrol.ScopeTeamsAll}},
|
||||
},
|
||||
{
|
||||
name: "robust to missing roles",
|
||||
identity: &authn.Identity{
|
||||
ID: "2", Type: claims.TypeUser, OrgID: 1,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
Roles: []string{"fixed:teams:reader", "fixed:unknown:role"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPermissions: map[string][]string{accesscontrol.ActionTeamsRead: {accesscontrol.ScopeTeamsAll}},
|
||||
},
|
||||
{
|
||||
name: "fetch permissions from permissions registry",
|
||||
identity: &authn.Identity{
|
||||
ID: "2", Type: claims.TypeUser, OrgID: 1,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
AllowedActions: []string{"dashboards:read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPermissions: map[string][]string{"dashboards:read": {"dashboards:uid:*", "folders:uid:*"}},
|
||||
},
|
||||
{
|
||||
name: "fetch scopeless permissions from permissions registry",
|
||||
identity: &authn.Identity{
|
||||
ID: "2", Type: claims.TypeUser, OrgID: 1,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
AllowedActions: []string{"test-app:read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPermissions: map[string][]string{"test-app:read": {""}},
|
||||
},
|
||||
{
|
||||
name: "robust to unknown actions",
|
||||
identity: &authn.Identity{
|
||||
ID: "2", Type: claims.TypeUser, OrgID: 1,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
AllowedActions: []string{"unknown:read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPermissions: map[string][]string{},
|
||||
},
|
||||
{
|
||||
name: "restrict permissions from roles and registry",
|
||||
identity: &authn.Identity{
|
||||
ID: "2", Type: claims.TypeUser, OrgID: 1,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
RestrictedActions: []string{accesscontrol.ActionUsersWrite, accesscontrol.ActionTeamsWrite, "dashboards:read"},
|
||||
AllowedActions: []string{"dashboards:read"},
|
||||
Roles: []string{"fixed:teams:reader", "fixed:teams:writer"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPermissions: map[string][]string{
|
||||
"dashboards:read": {"dashboards:uid:*", "folders:uid:*"},
|
||||
accesscontrol.ActionTeamsWrite: {accesscontrol.ScopeTeamsAll},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := setupTestEnv()
|
||||
s := setupTestEnv(t)
|
||||
|
||||
err := s.SyncPermissionsHook(context.Background(), tt.identity, &authn.Request{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(tt.identity.Permissions))
|
||||
assert.Equal(t, accesscontrol.GroupScopesByActionContext(context.Background(), tt.expectedPermissions), tt.identity.Permissions[tt.identity.OrgID])
|
||||
require.Equal(t, len(tt.expectedPermissions), len(tt.identity.Permissions[tt.identity.OrgID]))
|
||||
for action, scopes := range tt.expectedPermissions {
|
||||
require.ElementsMatch(t, scopes, tt.identity.Permissions[tt.identity.OrgID][action])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -242,18 +364,36 @@ func TestRBACSync_cloudRolesToAddAndRemove(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestEnv() *RBACSync {
|
||||
func setupTestEnv(t *testing.T) *RBACSync {
|
||||
acMock := &acmock.Mock{
|
||||
GetUserPermissionsFunc: func(ctx context.Context, siu identity.Requester, o accesscontrol.Options) ([]accesscontrol.Permission, error) {
|
||||
return []accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionUsersRead},
|
||||
{Action: accesscontrol.ActionUsersRead, Scope: accesscontrol.ScopeUsersAll},
|
||||
{Action: accesscontrol.ActionUsersWrite, Scope: accesscontrol.ScopeUsersAll},
|
||||
}, nil
|
||||
},
|
||||
GetRoleByNameFunc: func(ctx context.Context, i int64, s string) (*accesscontrol.RoleDTO, error) {
|
||||
if s == "fixed:teams:reader" {
|
||||
return &accesscontrol.RoleDTO{
|
||||
ID: 1, Name: "fixed:teams:reader",
|
||||
Permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll}},
|
||||
}, nil
|
||||
}
|
||||
if s == "fixed:teams:writer" {
|
||||
return &accesscontrol.RoleDTO{
|
||||
ID: 1, Name: "fixed:teams:writer",
|
||||
Permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsWrite, Scope: accesscontrol.ScopeTeamsAll}},
|
||||
}, nil
|
||||
}
|
||||
return nil, accesscontrol.ErrRoleNotFound
|
||||
},
|
||||
}
|
||||
permRegistry := permreg.ProvidePermissionRegistry(t)
|
||||
s := &RBACSync{
|
||||
ac: acMock,
|
||||
log: log.NewNopLogger(),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
ac: acMock,
|
||||
log: log.NewNopLogger(),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
permRegistry: permRegistry,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user