diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go index 492b3a7f9b0..18eade800d6 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go @@ -14,9 +14,10 @@ import ( func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessControlService { s := &OSSAccessControlService{ - Cfg: cfg, - UsageStats: usageStats, - Log: log.New("accesscontrol"), + Cfg: cfg, + UsageStats: usageStats, + Log: log.New("accesscontrol"), + scopeResolver: accesscontrol.NewScopeResolver(), } s.registerUsageMetrics() return s @@ -28,6 +29,7 @@ type OSSAccessControlService struct { UsageStats usagestats.Service Log log.Logger registrations accesscontrol.RegistrationList + scopeResolver accesscontrol.ScopeResolver } func (ac *OSSAccessControlService) IsDisabled() bool { @@ -65,6 +67,7 @@ func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.Si if err != nil { return false, err } + return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions)) } @@ -83,8 +86,12 @@ func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user continue } for _, p := range role.Permissions { - permission := p - permissions = append(permissions, &permission) + // if the permission has a keyword in its scope it will be resolved + permission, err := ac.scopeResolver.ResolveKeyword(user, p) + if err != nil { + return nil, err + } + permissions = append(permissions, permission) } } } diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go index ee3a8827105..6dc8a425805 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go @@ -45,6 +45,15 @@ func removeRoleHelper(role string) { } } +// extractRawPermissionsHelper extracts action and scope fields only from a permission slice +func extractRawPermissionsHelper(perms []*accesscontrol.Permission) []*accesscontrol.Permission { + res := make([]*accesscontrol.Permission, len(perms)) + for i, p := range perms { + res[i] = &accesscontrol.Permission{Action: p.Action, Scope: p.Scope} + } + return res +} + type evaluatingPermissionsTestCase struct { desc string user userTestCase @@ -490,3 +499,85 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) { }) } } + +func TestOSSAccessControlService_GetUserPermissions(t *testing.T) { + testUser := &models.SignedInUser{ + UserId: 2, + OrgId: 3, + OrgName: "TestOrg", + OrgRole: models.ROLE_VIEWER, + Login: "testUser", + Name: "Test User", + Email: "testuser@example.org", + } + registration := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Version: 1, + UID: "fixed:test:test", + Name: "fixed:test:test", + Description: "Test role", + Permissions: []accesscontrol.Permission{}, + }, + Grants: []string{"Viewer"}, + } + tests := []struct { + name string + user *models.SignedInUser + rawPerm accesscontrol.Permission + wantPerm accesscontrol.Permission + wantErr bool + }{ + { + name: "Translate orgs:current", + user: testUser, + rawPerm: accesscontrol.Permission{Action: "orgs:read", Scope: "orgs:current"}, + wantPerm: accesscontrol.Permission{Action: "orgs:read", Scope: "orgs:id:3"}, + wantErr: false, + }, + { + name: "Translate users:self", + user: testUser, + rawPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:self"}, + wantPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:id:2"}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Remove any inserted role after the test case has been run + t.Cleanup(func() { + removeRoleHelper(registration.Role.Name) + }) + + // Setup + ac := &OSSAccessControlService{ + Cfg: setting.NewCfg(), + UsageStats: &usagestats.UsageStatsMock{T: t}, + Log: log.New("accesscontrol-test"), + registrations: accesscontrol.RegistrationList{}, + scopeResolver: accesscontrol.NewScopeResolver(), + } + ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} + + registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm} + err := ac.DeclareFixedRoles(registration) + require.NoError(t, err) + + err = ac.RegisterFixedRoles() + require.NoError(t, err) + + // Test + userPerms, err := ac.GetUserPermissions(context.TODO(), tt.user) + if tt.wantErr { + assert.Error(t, err, "Expected an error with GetUserPermissions.") + return + } + require.NoError(t, err, "Did not expect an error with GetUserPermissions.") + + rawUserPerms := extractRawPermissionsHelper(userPerms) + + assert.Contains(t, rawUserPerms, &tt.wantPerm, "Expected resolution of raw permission") + assert.NotContains(t, rawUserPerms, &tt.rawPerm, "Expected raw permission to have been resolved") + }) + } +} diff --git a/pkg/services/accesscontrol/scoperesolution.go b/pkg/services/accesscontrol/scoperesolution.go new file mode 100644 index 00000000000..144e7e00b02 --- /dev/null +++ b/pkg/services/accesscontrol/scoperesolution.go @@ -0,0 +1,43 @@ +package accesscontrol + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/models" +) + +type KeywordScopeResolveFunc func(*models.SignedInUser) (string, error) + +// ScopeResolver contains a map of functions to resolve scope keywords such as `self` or `current` into `id` based scopes +type ScopeResolver struct { + keywordResolvers map[string]KeywordScopeResolveFunc +} + +func NewScopeResolver() ScopeResolver { + return ScopeResolver{ + keywordResolvers: map[string]KeywordScopeResolveFunc{ + "orgs:current": resolveCurrentOrg, + "users:self": resolveUserSelf, + }, + } +} + +func resolveCurrentOrg(u *models.SignedInUser) (string, error) { + return Scope("orgs", "id", fmt.Sprintf("%v", u.OrgId)), nil +} + +func resolveUserSelf(u *models.SignedInUser) (string, error) { + return Scope("users", "id", fmt.Sprintf("%v", u.UserId)), nil +} + +// ResolveKeyword resolves scope with keywords such as `self` or `current` into `id` based scopes +func (s *ScopeResolver) ResolveKeyword(user *models.SignedInUser, permission Permission) (*Permission, error) { + if fn, ok := s.keywordResolvers[permission.Scope]; ok { + resolvedScope, err := fn(user) + if err != nil { + return nil, fmt.Errorf("could not resolve %v: %v", permission.Scope, err) + } + permission.Scope = resolvedScope + } + return &permission, nil +} diff --git a/pkg/services/accesscontrol/scoperesolution_test.go b/pkg/services/accesscontrol/scoperesolution_test.go new file mode 100644 index 00000000000..b03e1ed7ea7 --- /dev/null +++ b/pkg/services/accesscontrol/scoperesolution_test.go @@ -0,0 +1,55 @@ +package accesscontrol + +import ( + "testing" + + "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" +) + +var testUser = &models.SignedInUser{ + UserId: 2, + OrgId: 3, + OrgName: "TestOrg", + OrgRole: models.ROLE_VIEWER, + Login: "testUser", + Name: "Test User", + Email: "testuser@example.org", +} + +func TestResolveKeywordedScope(t *testing.T) { + tests := []struct { + name string + user *models.SignedInUser + permission Permission + want *Permission + wantErr bool + }{ + { + name: "no scope", + user: testUser, + permission: Permission{Action: "users:read"}, + want: &Permission{Action: "users:read"}, + wantErr: false, + }, + { + name: "user if resolution", + user: testUser, + permission: Permission{Action: "users:read", Scope: "users:self"}, + want: &Permission{Action: "users:read", Scope: "users:id:2"}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolver := NewScopeResolver() + resolved, err := resolver.ResolveKeyword(tt.user, tt.permission) + if tt.wantErr { + assert.Error(t, err, "expected an error during the resolution of the scope") + return + } + assert.NoError(t, err) + assert.EqualValues(t, tt.want, resolved, "permission did not match expected resolution") + }) + } +}