diff --git a/pkg/services/accesscontrol/roles.go b/pkg/services/accesscontrol/roles.go index a81feff00ab..df358b03246 100644 --- a/pkg/services/accesscontrol/roles.go +++ b/pkg/services/accesscontrol/roles.go @@ -33,6 +33,10 @@ const ( FixedCloudViewerRole = "fixed:cloud:viewer" FixedCloudEditorRole = "fixed:cloud:editor" FixedCloudAdminRole = "fixed:cloud:admin" + + FixedCloudSupportTicketReader = "fixed:cloud:supportticket:reader" + FixedCloudSupportTicketEditor = "fixed:cloud:supportticket:editor" + FixedCloudSupportTicketAdmin = "fixed:cloud:supportticket:admin" ) // Roles definition diff --git a/pkg/services/authn/authnimpl/sync/rbac_sync.go b/pkg/services/authn/authnimpl/sync/rbac_sync.go index 9fc84962282..198dc020a7d 100644 --- a/pkg/services/authn/authnimpl/sync/rbac_sync.go +++ b/pkg/services/authn/authnimpl/sync/rbac_sync.go @@ -97,10 +97,44 @@ func (s *RBACSync) fetchPermissions(ctx context.Context, ident *authn.Identity) return permissions, nil } -var fixedCloudRoles = map[org.RoleType]string{ - org.RoleViewer: accesscontrol.FixedCloudViewerRole, - org.RoleEditor: accesscontrol.FixedCloudEditorRole, - org.RoleAdmin: accesscontrol.FixedCloudAdminRole, +func cloudRolesToAddAndRemove(ident *authn.Identity) ([]string, []string, error) { + const ( + expectedRolesToAddCount = 2 + rolesToRemoveInitialCap = 4 + ) + // Since Cloud Admin/Editor/Viewer roles are not yet implemented one-to-one in the Grafana, it becomes a confusing experience for users, + // therefore we are doing granular mapping of all available functionality in the Grafana temporary. + var fixedCloudRoles = map[org.RoleType][]string{ + org.RoleViewer: {accesscontrol.FixedCloudViewerRole, accesscontrol.FixedCloudSupportTicketReader}, + org.RoleEditor: {accesscontrol.FixedCloudEditorRole, accesscontrol.FixedCloudSupportTicketAdmin}, + org.RoleAdmin: {accesscontrol.FixedCloudAdminRole, accesscontrol.FixedCloudSupportTicketAdmin}, + } + + rolesToAdd := make([]string, 0, expectedRolesToAddCount) + rolesToRemove := make([]string, 0, rolesToRemoveInitialCap) + + currentRole := ident.GetOrgRole() + _, validRole := fixedCloudRoles[currentRole] + + if !validRole { + return nil, nil, errInvalidCloudRole.Errorf("invalid role: %s", currentRole) + } + + for role, fixedRoles := range fixedCloudRoles { + for _, fixedRole := range fixedRoles { + if role == currentRole { + rolesToAdd = append(rolesToAdd, fixedRole) + } else { + rolesToRemove = append(rolesToRemove, fixedRole) + } + } + } + + if len(rolesToAdd) != expectedRolesToAddCount { + return nil, nil, errInvalidCloudRole.Errorf("invalid role: %s", currentRole) + } + + return rolesToAdd, rolesToRemove, nil } func (s *RBACSync) SyncCloudRoles(ctx context.Context, ident *authn.Identity, r *authn.Request) error { @@ -122,19 +156,9 @@ func (s *RBACSync) SyncCloudRoles(ctx context.Context, ident *authn.Identity, r return err } - rolesToAdd := make([]string, 0, 1) - rolesToRemove := make([]string, 0, 2) - - for role, fixedRole := range fixedCloudRoles { - if role == ident.GetOrgRole() { - rolesToAdd = append(rolesToAdd, fixedRole) - } else { - rolesToRemove = append(rolesToRemove, fixedRole) - } - } - - if len(rolesToAdd) != 1 { - return errInvalidCloudRole.Errorf("invalid role: %s", ident.GetOrgRole()) + rolesToAdd, rolesToRemove, err := cloudRolesToAddAndRemove(ident) + if err != nil { + return err } return s.ac.SyncUserRoles(ctx, ident.GetOrgID(), accesscontrol.SyncUserRolesCommand{ diff --git a/pkg/services/authn/authnimpl/sync/rbac_sync_test.go b/pkg/services/authn/authnimpl/sync/rbac_sync_test.go index ef860bf4993..f69d6e52a32 100644 --- a/pkg/services/authn/authnimpl/sync/rbac_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/rbac_sync_test.go @@ -136,12 +136,102 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { req.SetMeta(authn.MetaKeyAuthModule, tt.module) err := s.SyncCloudRoles(context.Background(), tt.identity, req) - assert.ErrorIs(t, err, tt.expectedErr) + assert.ErrorIs(t, tt.expectedErr, err) assert.Equal(t, tt.expectedCalled, called) }) } } +func TestRBACSync_cloudRolesToAddAndRemove(t *testing.T) { + type testCase struct { + desc string + identity *authn.Identity + expectedErr error + expectedRolesToAdd []string + expectedRolesToRemove []string + } + + tests := []testCase{ + { + desc: "should map Cloud Viewer to Grafana Cloud Viewer and Support ticket reader", + identity: &authn.Identity{ + ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + }, + expectedErr: nil, + expectedRolesToAdd: []string{ + accesscontrol.FixedCloudViewerRole, + accesscontrol.FixedCloudSupportTicketReader, + }, + expectedRolesToRemove: []string{ + accesscontrol.FixedCloudEditorRole, + accesscontrol.FixedCloudSupportTicketAdmin, + accesscontrol.FixedCloudAdminRole, + accesscontrol.FixedCloudSupportTicketAdmin, + }, + }, + { + desc: "should map Cloud Editor to Grafana Cloud Editor and Support ticket admin", + identity: &authn.Identity{ + ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleEditor}, + }, + expectedErr: nil, + expectedRolesToAdd: []string{ + accesscontrol.FixedCloudEditorRole, + accesscontrol.FixedCloudSupportTicketAdmin, + }, + expectedRolesToRemove: []string{ + accesscontrol.FixedCloudViewerRole, + accesscontrol.FixedCloudSupportTicketReader, + accesscontrol.FixedCloudAdminRole, + accesscontrol.FixedCloudSupportTicketAdmin, + }, + }, + { + desc: "should map Cloud Admin to Grafana Cloud Admin and Support ticket admin", + identity: &authn.Identity{ + ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + }, + expectedErr: nil, + expectedRolesToAdd: []string{ + accesscontrol.FixedCloudAdminRole, + accesscontrol.FixedCloudSupportTicketAdmin, + }, + expectedRolesToRemove: []string{ + accesscontrol.FixedCloudViewerRole, + accesscontrol.FixedCloudSupportTicketReader, + accesscontrol.FixedCloudEditorRole, + accesscontrol.FixedCloudSupportTicketAdmin, + }, + }, + { + desc: "should return an error for not supported role", + identity: &authn.Identity{ + ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleNone}, + }, + expectedErr: errInvalidCloudRole, + expectedRolesToAdd: []string{}, + expectedRolesToRemove: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + rolesToAdd, rolesToRemove, err := cloudRolesToAddAndRemove(tt.identity) + assert.ErrorIs(t, tt.expectedErr, err) + assert.ElementsMatch(t, tt.expectedRolesToAdd, rolesToAdd) + assert.ElementsMatch(t, tt.expectedRolesToRemove, rolesToRemove) + }) + } +} + func setupTestEnv() *RBACSync { acMock := &acmock.Mock{ GetUserPermissionsFunc: func(ctx context.Context, siu authn.Requester, o accesscontrol.Options) ([]accesscontrol.Permission, error) {