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 <mgyongyosi@users.noreply.github.com>
Co-authored-by: Kalle <kalleep@users.noreply.github.com>
Co-authored-by: Jo <Jguer@users.noreply.github.com>
This commit is contained in:
linoman 2022-11-29 15:20:28 +01:00 committed by GitHub
parent 5b7ef92399
commit 6cc56311d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 85 additions and 4 deletions

View File

@ -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 {

View File

@ -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)}
}
}

View File

@ -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"`

View File

@ -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

View File

@ -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,

View File

@ -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)
}

View File

@ -478,6 +478,7 @@ func TestUserAuth(t *testing.T) {
}
type FakeAuthInfoStore struct {
login.AuthInfoService
ExpectedError error
ExpectedUser *user.User
ExpectedOAuth *models.UserAuth

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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', () => {

View File

@ -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> = (props) => {
<th>Role</th>
<th style={{ width: '34px' }} />
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@ -101,6 +103,12 @@ const UsersTable: FC<Props> = (props) => {
{user.isDisabled && <span className="label label-tag label-tag--gray">Disabled</span>}
</td>
<td className="width-1">
{Array.isArray(user.authLabels) && user.authLabels.length > 0 && (
<TagBadge label={user.authLabels[0]} removeIcon={false} count={0} />
)}
</td>
{contextSrv.hasPermissionInMetadata(AccessControlAction.OrgUsersRemove, user) && (
<td className="text-right">
<Button

View File

@ -15,6 +15,7 @@ export const getMockUsers = (amount: number) => {
role: 'Admin',
userId: i,
isDisabled: false,
authLabels: [],
});
}

View File

@ -12,6 +12,7 @@ export interface OrgUser extends WithAccessControlMetadata {
role: OrgRole;
userId: number;
isDisabled: boolean;
authLabels?: string[];
}
export interface User {