From 6cc56311d9b3b853fef3b3b0fdc85498db958b99 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Tue, 29 Nov 2022 15:20:28 +0100 Subject: [PATCH] Auth: Display id Provider label in orgs/users view (#58033) * Add frontend test * Add frontend label component * Adjust backend tests * Retrieve auth IDP labels for users at org/users. Co-authored-by: Misi Co-authored-by: Kalle Co-authored-by: Jo --- pkg/api/common_test.go | 7 ++++++ pkg/api/org_users.go | 20 ++++++++++++---- pkg/models/user_auth.go | 4 ++++ pkg/services/login/authinfo.go | 1 + .../authinfoservice/database/database.go | 24 +++++++++++++++++++ pkg/services/login/authinfoservice/service.go | 7 ++++++ .../login/authinfoservice/user_auth_test.go | 1 + pkg/services/login/logintest/logintest.go | 6 +++++ pkg/services/login/userprotection.go | 1 + pkg/services/oauthtoken/oauth_token_test.go | 1 + pkg/services/org/model.go | 1 + public/app/features/users/UsersTable.test.tsx | 6 +++++ public/app/features/users/UsersTable.tsx | 8 +++++++ .../app/features/users/__mocks__/userMocks.ts | 1 + public/app/types/user.ts | 1 + 15 files changed, 85 insertions(+), 4 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 2f4e57414b3..67230445046 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -41,6 +41,7 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/org" @@ -354,6 +355,9 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer { License: &licensing.OSSLicensingService{}, AccessControl: acimpl.ProvideAccessControl(cfg), annotationsRepo: annotationstest.NewFakeAnnotationsRepo(), + authInfoService: &logintest.AuthInfoServiceFake{ + ExpectedLabels: map[int64]string{int64(1): login.GetAuthProviderLabel(login.LDAPAuthModule)}, + }, } } @@ -431,6 +435,9 @@ func setupHTTPServerWithCfgDb( orgService: orgMock, teamService: teamService, annotationsRepo: annotationstest.NewFakeAnnotationsRepo(), + authInfoService: &logintest.AuthInfoServiceFake{ + ExpectedLabels: map[int64]string{int64(1): login.GetAuthProviderLabel(login.LDAPAuthModule)}, + }, } for _, o := range options { diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index d37b652d99c..7d229c6e33c 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" @@ -211,6 +212,7 @@ func (hs *HTTPServer) getOrgUsersHelper(c *models.ReqContext, query *org.GetOrgU filteredUsers := make([]*org.OrgUserDTO, 0, len(result)) userIDs := map[string]bool{} + authLabelsUserIDs := make([]int64, 0, len(result)) for _, user := range result { if dtos.IsHiddenUser(user.Login, signedInUser, hs.Cfg) { continue @@ -218,14 +220,24 @@ func (hs *HTTPServer) getOrgUsersHelper(c *models.ReqContext, query *org.GetOrgU user.AvatarURL = dtos.GetGravatarUrl(user.Email) userIDs[fmt.Sprint(user.UserID)] = true + authLabelsUserIDs = append(authLabelsUserIDs, user.UserID) filteredUsers = append(filteredUsers, user) } - // Get accesscontrol metadata for users in the target org + modules, err := hs.authInfoService.GetUserLabels(c.Req.Context(), models.GetUserLabelsQuery{ + UserIDs: authLabelsUserIDs, + }) + + if err != nil { + hs.log.Warn("failed to retrieve users IDP label", err) + } + + // Get accesscontrol metadata and IPD labels for users in the target org accessControlMetadata := hs.getMultiAccessControlMetadata(c, query.OrgID, "users:id:", userIDs) - if len(accessControlMetadata) > 0 { - for i := range filteredUsers { - filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserID)] + for i := range filteredUsers { + filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserID)] + if module, ok := modules[filteredUsers[i].UserID]; ok { + filteredUsers[i].AuthLabels = []string{login.GetAuthProviderLabel(module)} } } diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go index 9bbd3b62a13..ec1f70e75a0 100644 --- a/pkg/models/user_auth.go +++ b/pkg/models/user_auth.go @@ -125,6 +125,10 @@ type GetAuthInfoQuery struct { Result *UserAuth } +type GetUserLabelsQuery struct { + UserIDs []int64 +} + type TeamOrgGroupDTO struct { TeamName string `json:"teamName"` OrgName string `json:"orgName"` diff --git a/pkg/services/login/authinfo.go b/pkg/services/login/authinfo.go index e8484c781ba..7ef0e559cbc 100644 --- a/pkg/services/login/authinfo.go +++ b/pkg/services/login/authinfo.go @@ -10,6 +10,7 @@ import ( type AuthInfoService interface { LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*user.User, error) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error + GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error diff --git a/pkg/services/login/authinfoservice/database/database.go b/pkg/services/login/authinfoservice/database/database.go index a7fdf2fd65c..77540f475ce 100644 --- a/pkg/services/login/authinfoservice/database/database.go +++ b/pkg/services/login/authinfoservice/database/database.go @@ -108,6 +108,30 @@ func (s *AuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthIn return nil } +func (s *AuthInfoStore) GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) { + userAuths := []models.UserAuth{} + params := make([]interface{}, 0, len(query.UserIDs)) + for _, id := range query.UserIDs { + params = append(params, id) + } + + err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { + return sess.Table("user_auth").In("user_id", params).OrderBy("created").Find(&userAuths) + }) + + if err != nil { + return nil, err + } + + labelMap := make(map[int64]string, len(userAuths)) + + for i := range userAuths { + labelMap[userAuths[i].UserId] = userAuths[i].AuthModule + } + + return labelMap, nil +} + func (s *AuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { authUser := &models.UserAuth{ UserId: cmd.UserId, diff --git a/pkg/services/login/authinfoservice/service.go b/pkg/services/login/authinfoservice/service.go index 3a6025bd491..76efa7ca8b0 100644 --- a/pkg/services/login/authinfoservice/service.go +++ b/pkg/services/login/authinfoservice/service.go @@ -185,6 +185,13 @@ func (s *Implementation) GetAuthInfo(ctx context.Context, query *models.GetAuthI return s.authInfoStore.GetAuthInfo(ctx, query) } +func (s *Implementation) GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) { + if len(query.UserIDs) == 0 { + return map[int64]string{}, nil + } + return s.authInfoStore.GetUserLabels(ctx, query) +} + func (s *Implementation) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { return s.authInfoStore.UpdateAuthInfo(ctx, cmd) } diff --git a/pkg/services/login/authinfoservice/user_auth_test.go b/pkg/services/login/authinfoservice/user_auth_test.go index 525e6167ac0..5e8181e9528 100644 --- a/pkg/services/login/authinfoservice/user_auth_test.go +++ b/pkg/services/login/authinfoservice/user_auth_test.go @@ -478,6 +478,7 @@ func TestUserAuth(t *testing.T) { } type FakeAuthInfoStore struct { + login.AuthInfoService ExpectedError error ExpectedUser *user.User ExpectedOAuth *models.UserAuth diff --git a/pkg/services/login/logintest/logintest.go b/pkg/services/login/logintest/logintest.go index 6eda3dd1d14..c83795ea18d 100644 --- a/pkg/services/login/logintest/logintest.go +++ b/pkg/services/login/logintest/logintest.go @@ -19,11 +19,13 @@ func (l *LoginServiceFake) DisableExternalUser(ctx context.Context, username str func (l *LoginServiceFake) SetTeamSyncFunc(login.TeamSyncFunc) {} type AuthInfoServiceFake struct { + login.AuthInfoService LatestUserID int64 ExpectedUserAuth *models.UserAuth ExpectedUser *user.User ExpectedExternalUser *models.ExternalUserInfo ExpectedError error + ExpectedLabels map[int64]string } func (a *AuthInfoServiceFake) LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*user.User, error) { @@ -41,6 +43,10 @@ func (a *AuthInfoServiceFake) GetAuthInfo(ctx context.Context, query *models.Get return a.ExpectedError } +func (a *AuthInfoServiceFake) GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) { + return a.ExpectedLabels, a.ExpectedError +} + func (a *AuthInfoServiceFake) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { return a.ExpectedError } diff --git a/pkg/services/login/userprotection.go b/pkg/services/login/userprotection.go index 83146ce2bdf..f77b6401244 100644 --- a/pkg/services/login/userprotection.go +++ b/pkg/services/login/userprotection.go @@ -14,6 +14,7 @@ type UserProtectionService interface { type Store interface { GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error + GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error diff --git a/pkg/services/oauthtoken/oauth_token_test.go b/pkg/services/oauthtoken/oauth_token_test.go index ae761f0580f..063de3ce3f2 100644 --- a/pkg/services/oauthtoken/oauth_token_test.go +++ b/pkg/services/oauthtoken/oauth_token_test.go @@ -304,6 +304,7 @@ func (m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token) } type FakeAuthInfoStore struct { + login.Store ExpectedError error ExpectedUser *user.User ExpectedOAuth *models.UserAuth diff --git a/pkg/services/org/model.go b/pkg/services/org/model.go index d6956ce2384..5e20ee43c7e 100644 --- a/pkg/services/org/model.go +++ b/pkg/services/org/model.go @@ -148,6 +148,7 @@ type OrgUserDTO struct { LastSeenAtAge string `json:"lastSeenAtAge"` AccessControl map[string]bool `json:"accessControl,omitempty"` IsDisabled bool `json:"isDisabled"` + AuthLabels []string `json:"authLabels" xorm:"-"` } type RemoveOrgUserCommand struct { diff --git a/public/app/features/users/UsersTable.test.tsx b/public/app/features/users/UsersTable.test.tsx index 44471a5f720..d7ef2b4181d 100644 --- a/public/app/features/users/UsersTable.test.tsx +++ b/public/app/features/users/UsersTable.test.tsx @@ -48,6 +48,12 @@ describe('Render', () => { expect(screen.getByText('Disabled')).toBeInTheDocument(); }); + it('should render LDAP label', () => { + const usersData = getMockUsers(5); + usersData[0].authLabels = ['LDAP']; + setup({ users: usersData }); + expect(screen.getByText(usersData[0].authLabels[0])).toBeInTheDocument(); + }); }); describe('Remove modal', () => { diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx index 0c9694cbbb1..fa437a41145 100644 --- a/public/app/features/users/UsersTable.tsx +++ b/public/app/features/users/UsersTable.tsx @@ -4,6 +4,7 @@ import { OrgRole } from '@grafana/data'; import { Button, ConfirmModal } from '@grafana/ui'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; +import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { contextSrv } from 'app/core/core'; import { AccessControlAction, OrgUser, Role } from 'app/types'; @@ -50,6 +51,7 @@ const UsersTable: FC = (props) => { Role + @@ -101,6 +103,12 @@ const UsersTable: FC = (props) => { {user.isDisabled && Disabled} + + {Array.isArray(user.authLabels) && user.authLabels.length > 0 && ( + + )} + + {contextSrv.hasPermissionInMetadata(AccessControlAction.OrgUsersRemove, user) && (