Users: Allow specifying user UIDs in params (#95424)

* add user ID API translation

* add uid to user frontend

* use users' UIDs in admin pages

* fix ldapSync page

* use global user search for user by UID

* remove active org filtering

* remove orgID params
This commit is contained in:
Jo 2024-10-30 14:14:42 +01:00 committed by GitHub
parent f0391e31d2
commit 90d2f4659e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 175 additions and 93 deletions

View File

@ -71,6 +71,7 @@ func (hs *HTTPServer) AdminCreateUser(c *contextmodel.ReqContext) response.Respo
result := user.AdminCreateUserResponse{
Message: "User created",
ID: usr.ID,
UID: usr.UID,
}
return response.JSON(http.StatusOK, result)

View File

@ -30,6 +30,9 @@
package api
import (
"errors"
"net/http"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
@ -37,6 +40,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/ssoutils"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/auth"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/correlations"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
@ -46,6 +50,7 @@ import (
publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web"
"go.opentelemetry.io/otel"
)
@ -65,6 +70,7 @@ func (hs *HTTPServer) registerRoutes() {
authorize := ac.Middleware(hs.AccessControl)
authorizeInOrg := ac.AuthorizeInOrgMiddleware(hs.AccessControl, hs.authnService)
quota := middleware.Quota(hs.QuotaService)
userUIDResolver := middlewareUserUIDResolver(hs.userService, ":id")
r := hs.RouteRegister
@ -282,13 +288,13 @@ func (hs *HTTPServer) registerRoutes() {
userIDScope := ac.Scope("global.users", "id", ac.Parameter(":id"))
usersRoute.Get("/", authorize(ac.EvalPermission(ac.ActionUsersRead)), routing.Wrap(hs.searchUsersService.SearchUsers))
usersRoute.Get("/search", authorize(ac.EvalPermission(ac.ActionUsersRead)), routing.Wrap(hs.searchUsersService.SearchUsersWithPaging))
usersRoute.Get("/:id", authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserByID))
usersRoute.Get("/:id/teams", authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserTeams))
usersRoute.Get("/:id/orgs", authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserOrgList))
usersRoute.Get("/:id", userUIDResolver, authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserByID))
usersRoute.Get("/:id/teams", userUIDResolver, authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserTeams))
usersRoute.Get("/:id/orgs", userUIDResolver, authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserOrgList))
// query parameters /users/lookup?loginOrEmail=admin@example.com
usersRoute.Get("/lookup", authorize(ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), routing.Wrap(hs.GetUserByLoginOrEmail))
usersRoute.Put("/:id", authorize(ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(hs.UpdateUser))
usersRoute.Post("/:id/using/:orgId", authorize(ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(hs.UpdateUserActiveOrg))
usersRoute.Put("/:id", userUIDResolver, authorize(ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(hs.UpdateUser))
usersRoute.Post("/:id/using/:orgId", userUIDResolver, authorize(ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(hs.UpdateUserActiveOrg))
}, requestmeta.SetOwner(requestmeta.TeamAuth))
// org information available to all users.
@ -355,6 +361,7 @@ func (hs *HTTPServer) registerRoutes() {
// search all orgs
apiRoute.Get("/orgs", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionOrgsRead)), routing.Wrap(hs.SearchOrgs))
orgUserUIDResolver := middlewareUserUIDResolver(hs.userService, ":userId")
// orgs (admin routes)
apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
userIDScope := ac.Scope("users", "id", ac.Parameter(":userId"))
@ -365,8 +372,8 @@ func (hs *HTTPServer) registerRoutes() {
orgsRoute.Get("/users", requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsers))
orgsRoute.Get("/users/search", requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.SearchOrgUsers))
orgsRoute.Post("/users", requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), routing.Wrap(hs.AddOrgUser))
orgsRoute.Patch("/users/:userId", requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersWrite, userIDScope)), routing.Wrap(hs.UpdateOrgUser))
orgsRoute.Delete("/users/:userId", requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUser))
orgsRoute.Patch("/users/:userId", orgUserUIDResolver, requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersWrite, userIDScope)), routing.Wrap(hs.UpdateOrgUser))
orgsRoute.Delete("/users/:userId", orgUserUIDResolver, requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUser))
orgsRoute.Get("/quotas", authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsQuotasRead)), routing.Wrap(hs.GetOrgQuotas))
orgsRoute.Put("/quotas/:target", authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsQuotasWrite)), routing.Wrap(hs.UpdateOrgQuota))
})
@ -577,17 +584,17 @@ func (hs *HTTPServer) registerRoutes() {
userIDScope := ac.Scope("global.users", "id", ac.Parameter(":id"))
adminUserRoute.Post("/", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.AdminCreateUser))
adminUserRoute.Put("/:id/password", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersPasswordUpdate, userIDScope)), routing.Wrap(hs.AdminUpdateUserPassword))
adminUserRoute.Put("/:id/permissions", reqGrafanaAdmin, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersPermissionsUpdate, userIDScope)), routing.Wrap(hs.AdminUpdateUserPermissions))
adminUserRoute.Delete("/:id", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersDelete, userIDScope)), routing.Wrap(hs.AdminDeleteUser))
adminUserRoute.Post("/:id/disable", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersDisable, userIDScope)), routing.Wrap(hs.AdminDisableUser))
adminUserRoute.Post("/:id/enable", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersEnable, userIDScope)), routing.Wrap(hs.AdminEnableUser))
adminUserRoute.Get("/:id/quotas", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersQuotasList, userIDScope)), routing.Wrap(hs.GetUserQuotas))
adminUserRoute.Put("/:id/quotas/:target", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersQuotasUpdate, userIDScope)), routing.Wrap(hs.UpdateUserQuota))
adminUserRoute.Put("/:id/password", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersPasswordUpdate, userIDScope)), routing.Wrap(hs.AdminUpdateUserPassword))
adminUserRoute.Put("/:id/permissions", userUIDResolver, reqGrafanaAdmin, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersPermissionsUpdate, userIDScope)), routing.Wrap(hs.AdminUpdateUserPermissions))
adminUserRoute.Delete("/:id", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersDelete, userIDScope)), routing.Wrap(hs.AdminDeleteUser))
adminUserRoute.Post("/:id/disable", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersDisable, userIDScope)), routing.Wrap(hs.AdminDisableUser))
adminUserRoute.Post("/:id/enable", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersEnable, userIDScope)), routing.Wrap(hs.AdminEnableUser))
adminUserRoute.Get("/:id/quotas", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersQuotasList, userIDScope)), routing.Wrap(hs.GetUserQuotas))
adminUserRoute.Put("/:id/quotas/:target", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersQuotasUpdate, userIDScope)), routing.Wrap(hs.UpdateUserQuota))
adminUserRoute.Post("/:id/logout", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersLogout, userIDScope)), routing.Wrap(hs.AdminLogoutUser))
adminUserRoute.Get("/:id/auth-tokens", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersAuthTokenList, userIDScope)), routing.Wrap(hs.AdminGetUserAuthTokens))
adminUserRoute.Post("/:id/revoke-auth-token", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersAuthTokenUpdate, userIDScope)), routing.Wrap(hs.AdminRevokeUserAuthToken))
adminUserRoute.Post("/:id/logout", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersLogout, userIDScope)), routing.Wrap(hs.AdminLogoutUser))
adminUserRoute.Get("/:id/auth-tokens", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersAuthTokenList, userIDScope)), routing.Wrap(hs.AdminGetUserAuthTokens))
adminUserRoute.Post("/:id/revoke-auth-token", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersAuthTokenUpdate, userIDScope)), routing.Wrap(hs.AdminRevokeUserAuthToken))
}, reqSignedIn)
// rendering
@ -606,3 +613,23 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey))
r.Delete("/api/snapshots/:key", reqSignedIn, routing.Wrap(hs.DeleteDashboardSnapshot))
}
func middlewareUserUIDResolver(userService user.Service, paramName string) web.Handler {
handler := user.UIDToIDHandler(userService)
return func(c *contextmodel.ReqContext) {
userID := web.Params(c.Req)[paramName]
id, err := handler(c.Req.Context(), userID)
if err == nil {
gotParams := web.Params(c.Req)
gotParams[paramName] = id
web.SetURLParams(c.Req, gotParams)
} else {
if errors.Is(err, user.ErrUserNotFound) {
c.JsonApiErr(http.StatusNotFound, "User not found", nil)
} else {
c.JsonApiErr(http.StatusInternalServerError, "Failed to resolve user", err)
}
}
}
}

View File

@ -856,10 +856,10 @@ func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item un
// #TODO refactor the various conversions of the folder so that we either set created by in folder.Folder or
// we convert from unstructured to folder DTO without an intermediate conversion to folder.Folder
if len(fDTO.CreatedBy) > 0 {
creator = fk8s.getUserLogin(ctx, toUID(fDTO.CreatedBy), orgID)
creator = fk8s.getUserLogin(ctx, toUID(fDTO.CreatedBy))
}
if len(fDTO.UpdatedBy) > 0 {
updater = fk8s.getUserLogin(ctx, toUID(fDTO.UpdatedBy), orgID)
updater = fk8s.getUserLogin(ctx, toUID(fDTO.UpdatedBy))
}
acMetadata, _ := fk8s.getFolderACMetadata(c, fold)
@ -930,13 +930,12 @@ func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item un
return folderDTO, nil
}
func (fk8s *folderK8sHandler) getUserLogin(ctx context.Context, userUID string, orgID int64) string {
func (fk8s *folderK8sHandler) getUserLogin(ctx context.Context, userUID string) string {
ctx, span := tracer.Start(ctx, "api.getUserLogin")
defer span.End()
query := user.GetUserByUIDQuery{
UID: userUID,
OrgID: orgID,
UID: userUID,
}
user, err := fk8s.userService.GetByUID(ctx, &query)
if err != nil {

View File

@ -1,6 +1,7 @@
package resourcepermissions
import (
"errors"
"fmt"
"net/http"
"strconv"
@ -14,6 +15,7 @@ import (
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
@ -44,6 +46,7 @@ func (a *api) registerEndpoints() {
licenseMW = nopMiddleware
}
userUIDResolver := middlewareUserUIDResolver(a.service.userService, ":userID")
teamUIDResolver := team.MiddlewareTeamUIDResolver(a.service.teamService, ":teamID")
resourceResolver := func(resTranslator ResourceTranslator) web.Handler {
return func(c *contextmodel.ReqContext) {
@ -72,7 +75,7 @@ func (a *api) registerEndpoints() {
r.Get("/:resourceID", resourceResolver, auth(accesscontrol.EvalPermission(actionRead, scope)), routing.Wrap(a.getPermissions))
r.Post("/:resourceID", resourceResolver, licenseMW, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setPermissions))
if a.service.options.Assignments.Users {
r.Post("/:resourceID/users/:userID", licenseMW, resourceResolver, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setUserPermission))
r.Post("/:resourceID/users/:userID", licenseMW, resourceResolver, userUIDResolver, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setUserPermission))
}
if a.service.options.Assignments.Teams {
r.Post("/:resourceID/teams/:teamID", licenseMW, resourceResolver, teamUIDResolver, auth(accesscontrol.EvalPermission(actionWrite, scope)), routing.Wrap(a.setTeamPermission))
@ -445,3 +448,24 @@ func permissionSetResponse(cmd setPermissionCommand) response.Response {
}
return response.Success(message)
}
// middlewareUserUIDResolver resolves the user UID to ID and sets the ID in the URL params.
func middlewareUserUIDResolver(userService user.Service, paramName string) web.Handler {
handler := user.UIDToIDHandler(userService)
return func(c *contextmodel.ReqContext) {
userID := web.Params(c.Req)[paramName]
id, err := handler(c.Req.Context(), userID)
if err == nil {
gotParams := web.Params(c.Req)
gotParams[paramName] = id
web.SetURLParams(c.Req, gotParams)
} else {
if errors.Is(err, user.ErrUserNotFound) {
c.JsonApiErr(http.StatusNotFound, "User not found", nil)
} else {
c.JsonApiErr(http.StatusInternalServerError, "Failed to resolve user", err)
}
}
}
}

View File

@ -213,8 +213,7 @@ type GetUserByIDQuery struct {
}
type GetUserByUIDQuery struct {
OrgID int64
UID string
UID string
}
type StartVerifyEmailCommand struct {
@ -264,6 +263,7 @@ const (
type AdminCreateUserResponse struct {
ID int64 `json:"id"`
UID string `json:"uid"`
Message string `json:"message"`
}

View File

@ -2,6 +2,7 @@ package user
import (
"context"
"strconv"
"github.com/grafana/grafana/pkg/registry"
)
@ -13,6 +14,7 @@ type Service interface {
CreateServiceAccount(context.Context, *CreateUserCommand) (*User, error)
Delete(context.Context, *DeleteUserCommand) error
GetByID(context.Context, *GetUserByIDQuery) (*User, error)
// GetByUID returns a user by UID. This also includes service accounts (identity use only)
GetByUID(context.Context, *GetUserByUIDQuery) (*User, error)
GetByLogin(context.Context, *GetUserByLoginQuery) (*User, error)
GetByEmail(context.Context, *GetUserByEmailQuery) (*User, error)
@ -28,3 +30,16 @@ type Verifier interface {
Start(ctx context.Context, cmd StartVerifyEmailCommand) error
Complete(ctx context.Context, cmd CompleteEmailVerifyCommand) error
}
func UIDToIDHandler(userService Service) func(ctx context.Context, userID string) (string, error) {
return func(ctx context.Context, userID string) (string, error) {
_, err := strconv.ParseInt(userID, 10, 64)
if userID == "" || err == nil {
return userID, nil
}
user, err := userService.GetByUID(ctx, &GetUserByUIDQuery{
UID: userID,
})
return strconv.FormatInt(user.ID, 10), err
}
}

View File

@ -20,7 +20,7 @@ import (
type store interface {
Insert(context.Context, *user.User) (int64, error)
GetByID(context.Context, int64) (*user.User, error)
GetByUID(ctx context.Context, orgId int64, uid string) (*user.User, error)
GetByUID(ctx context.Context, uid string) (*user.User, error)
GetByLogin(context.Context, *user.GetUserByLoginQuery) (*user.User, error)
GetByEmail(context.Context, *user.GetUserByEmailQuery) (*user.User, error)
Delete(context.Context, int64) error
@ -108,14 +108,11 @@ func (ss *sqlStore) GetByID(ctx context.Context, userID int64) (*user.User, erro
return &usr, err
}
func (ss *sqlStore) GetByUID(ctx context.Context, orgId int64, uid string) (*user.User, error) {
func (ss *sqlStore) GetByUID(ctx context.Context, uid string) (*user.User, error) {
var usr user.User
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
has, err := sess.Table("user").
Where("org_id = ? AND uid = ?", orgId, uid).
Get(&usr)
has, err := sess.Table("user").Where("uid = ?", uid).Get(&usr)
if err != nil {
return err
} else if !has {

View File

@ -99,6 +99,12 @@ func TestIntegrationUserDataAccess(t *testing.T) {
})
require.NoError(t, err)
require.Equal(t, "abcd", siu.UserUID)
query := user.GetUserByUIDQuery{UID: "abcd"}
result, err := userStore.GetByUID(context.Background(), query.UID)
require.Nil(t, err)
require.Equal(t, result.UID, "abcd")
require.Equal(t, result.Email, "next-test@email.com")
})
t.Run("Testing DB - creates and loads user", func(t *testing.T) {

View File

@ -214,12 +214,11 @@ func (s *Service) GetByID(ctx context.Context, query *user.GetUserByIDQuery) (*u
func (s *Service) GetByUID(ctx context.Context, query *user.GetUserByUIDQuery) (*user.User, error) {
ctx, span := s.tracer.Start(ctx, "user.GetByUID", trace.WithAttributes(
attribute.Int64("orgID", query.OrgID),
attribute.String("userUID", query.UID),
))
defer span.End()
return s.store.GetByUID(ctx, query.OrgID, query.UID)
return s.store.GetByUID(ctx, query.UID)
}
func (s *Service) GetByLogin(ctx context.Context, query *user.GetUserByLoginQuery) (*user.User, error) {

View File

@ -291,7 +291,7 @@ func (f *FakeUserStore) GetByID(context.Context, int64) (*user.User, error) {
return f.ExpectedUser, f.ExpectedError
}
func (f *FakeUserStore) GetByUID(context.Context, int64, string) (*user.User, error) {
func (f *FakeUserStore) GetByUID(context.Context, string) (*user.User, error) {
return f.ExpectedUser, f.ExpectedError
}

View File

@ -2717,6 +2717,9 @@
},
"message": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},

View File

@ -12439,6 +12439,9 @@
},
"message": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},

View File

@ -61,31 +61,30 @@ export const UserAdminPage = ({
}: Props) => {
const { id = '' } = useParams();
useEffect(() => {
const userId = parseInt(id, 10);
loadAdminUserPage(userId);
loadAdminUserPage(id);
}, [id, loadAdminUserPage]);
const onPasswordChange = (password: string) => {
if (user) {
setUserPassword(user.id, password);
setUserPassword(user.uid, password);
}
};
const onGrafanaAdminChange = (isGrafanaAdmin: boolean) => {
if (user) {
updateUserPermissions(user.id, isGrafanaAdmin);
updateUserPermissions(user.uid, isGrafanaAdmin);
}
};
const onOrgRemove = (orgId: number) => {
if (user) {
deleteOrgUser(user.id, orgId);
deleteOrgUser(user.uid, orgId);
}
};
const onOrgRoleChange = (orgId: number, newRole: string) => {
if (user) {
updateOrgUserRole(user.id, orgId, newRole);
updateOrgUserRole(user.uid, orgId, newRole);
}
};
@ -97,19 +96,19 @@ export const UserAdminPage = ({
const onSessionRevoke = (tokenId: number) => {
if (user) {
revokeSession(tokenId, user.id);
revokeSession(tokenId, user.uid);
}
};
const onAllSessionsRevoke = () => {
if (user) {
revokeAllSessions(user.id);
revokeAllSessions(user.uid);
}
};
const onUserSync = () => {
if (user) {
syncLdapUser(user.id);
syncLdapUser(user.id, user.uid);
}
};

View File

@ -33,9 +33,9 @@ const UserCreatePage = () => {
const onSubmit = useCallback(
async (data: UserDTO) => {
const { id } = await createUser(data);
const { uid } = await createUser(data);
navigate(`/admin/users/edit/${id}`);
navigate(`/admin/users/edit/${uid}`);
},
[navigate]
);

View File

@ -10,9 +10,9 @@ interface Props {
user: UserDTO;
onUserUpdate: (user: UserDTO) => void;
onUserDelete: (userId: number) => void;
onUserDisable: (userId: number) => void;
onUserEnable: (userId: number) => void;
onUserDelete: (userUid: string) => void;
onUserDisable: (userUid: string) => void;
onUserEnable: (userUid: string) => void;
onPasswordChange(password: string): void;
}
@ -43,11 +43,11 @@ export function UserProfile({
}
};
const handleUserDelete = () => onUserDelete(user.id);
const handleUserDelete = () => onUserDelete(user.uid);
const handleUserDisable = () => onUserDisable(user.id);
const handleUserDisable = () => onUserDisable(user.uid);
const handleUserEnable = () => onUserEnable(user.id);
const handleUserEnable = () => onUserEnable(user.uid);
const onUserNameChange = (newValue: string) => {
onUserUpdate({

View File

@ -54,7 +54,7 @@ export const UsersTable = ({
header: 'Login',
cell: ({ row: { original } }: Cell<'login'>) => {
return (
<TextLink color="primary" inline={false} href={`/admin/users/edit/${original.id}`} title="Edit user">
<TextLink color="primary" inline={false} href={`/admin/users/edit/${original.uid}`} title="Edit user">
{original.login}
</TextLink>
);
@ -146,7 +146,7 @@ export const UsersTable = ({
variant="secondary"
size="sm"
icon="pen"
href={`admin/users/edit/${original.id}`}
href={`admin/users/edit/${original.uid}`}
aria-label={`Edit user ${original.name}`}
tooltip={'Edit user'}
/>

View File

@ -43,13 +43,13 @@ import {
} from './reducers';
// UserAdminPage
export function loadAdminUserPage(userId: number): ThunkResult<void> {
export function loadAdminUserPage(userUid: string): ThunkResult<void> {
return async (dispatch) => {
try {
dispatch(userAdminPageLoadedAction(false));
await dispatch(loadUserProfile(userId));
await dispatch(loadUserOrgs(userId));
await dispatch(loadUserSessions(userId));
await dispatch(loadUserProfile(userUid));
await dispatch(loadUserOrgs(userUid));
await dispatch(loadUserSessions(userUid));
if (config.ldapEnabled && featureEnabled('ldapsync')) {
await dispatch(loadLdapSyncStatus());
}
@ -69,60 +69,60 @@ export function loadAdminUserPage(userId: number): ThunkResult<void> {
};
}
export function loadUserProfile(userId: number): ThunkResult<void> {
export function loadUserProfile(userUid: string): ThunkResult<void> {
return async (dispatch) => {
const user = await getBackendSrv().get(`/api/users/${userId}`, accessControlQueryParam());
const user = await getBackendSrv().get(`/api/users/${userUid}`, accessControlQueryParam());
dispatch(userProfileLoadedAction(user));
};
}
export function updateUser(user: UserDTO): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().put(`/api/users/${user.id}`, user);
dispatch(loadAdminUserPage(user.id));
await getBackendSrv().put(`/api/users/${user.uid}`, user);
dispatch(loadAdminUserPage(user.uid));
};
}
export function setUserPassword(userId: number, password: string): ThunkResult<void> {
export function setUserPassword(userUid: string, password: string): ThunkResult<void> {
return async (dispatch) => {
const payload = { password };
await getBackendSrv().put(`/api/admin/users/${userId}/password`, payload);
dispatch(loadAdminUserPage(userId));
await getBackendSrv().put(`/api/admin/users/${userUid}/password`, payload);
dispatch(loadAdminUserPage(userUid));
};
}
export function disableUser(userId: number): ThunkResult<void> {
export function disableUser(userUid: string): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().post(`/api/admin/users/${userId}/disable`);
await getBackendSrv().post(`/api/admin/users/${userUid}/disable`);
locationService.push('/admin/users');
};
}
export function enableUser(userId: number): ThunkResult<void> {
export function enableUser(userUid: string): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().post(`/api/admin/users/${userId}/enable`);
dispatch(loadAdminUserPage(userId));
await getBackendSrv().post(`/api/admin/users/${userUid}/enable`);
dispatch(loadAdminUserPage(userUid));
};
}
export function deleteUser(userId: number): ThunkResult<void> {
export function deleteUser(userUid: string): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().delete(`/api/admin/users/${userId}`);
await getBackendSrv().delete(`/api/admin/users/${userUid}`);
locationService.push('/admin/users');
};
}
export function updateUserPermissions(userId: number, isGrafanaAdmin: boolean): ThunkResult<void> {
export function updateUserPermissions(userUid: string, isGrafanaAdmin: boolean): ThunkResult<void> {
return async (dispatch) => {
const payload = { isGrafanaAdmin };
await getBackendSrv().put(`/api/admin/users/${userId}/permissions`, payload);
dispatch(loadAdminUserPage(userId));
await getBackendSrv().put(`/api/admin/users/${userUid}/permissions`, payload);
dispatch(loadAdminUserPage(userUid));
};
}
export function loadUserOrgs(userId: number): ThunkResult<void> {
export function loadUserOrgs(userUid: string): ThunkResult<void> {
return async (dispatch) => {
const orgs = await getBackendSrv().get(`/api/users/${userId}/orgs`);
const orgs = await getBackendSrv().get(`/api/users/${userUid}/orgs`);
dispatch(userOrgsLoadedAction(orgs));
};
}
@ -134,32 +134,32 @@ export function addOrgUser(user: UserDTO, orgId: number, role: string): ThunkRes
role: role,
};
await getBackendSrv().post(`/api/orgs/${orgId}/users/`, payload);
dispatch(loadAdminUserPage(user.id));
dispatch(loadAdminUserPage(user.uid));
};
}
export function updateOrgUserRole(userId: number, orgId: number, role: string): ThunkResult<void> {
export function updateOrgUserRole(userUid: string, orgId: number, role: string): ThunkResult<void> {
return async (dispatch) => {
const payload = { role };
await getBackendSrv().patch(`/api/orgs/${orgId}/users/${userId}`, payload);
dispatch(loadAdminUserPage(userId));
await getBackendSrv().patch(`/api/orgs/${orgId}/users/${userUid}`, payload);
dispatch(loadAdminUserPage(userUid));
};
}
export function deleteOrgUser(userId: number, orgId: number): ThunkResult<void> {
export function deleteOrgUser(userUid: string, orgId: number): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().delete(`/api/orgs/${orgId}/users/${userId}`);
dispatch(loadAdminUserPage(userId));
await getBackendSrv().delete(`/api/orgs/${orgId}/users/${userUid}`);
dispatch(loadAdminUserPage(userUid));
};
}
export function loadUserSessions(userId: number): ThunkResult<void> {
export function loadUserSessions(userUid: string): ThunkResult<void> {
return async (dispatch) => {
if (!contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList)) {
return;
}
const tokens = await getBackendSrv().get(`/api/admin/users/${userId}/auth-tokens`);
const tokens = await getBackendSrv().get(`/api/admin/users/${userUid}/auth-tokens`);
tokens.reverse();
const sessions = tokens.map((session: UserSession) => {
@ -182,18 +182,18 @@ export function loadUserSessions(userId: number): ThunkResult<void> {
};
}
export function revokeSession(tokenId: number, userId: number): ThunkResult<void> {
export function revokeSession(tokenId: number, userUid: string): ThunkResult<void> {
return async (dispatch) => {
const payload = { authTokenId: tokenId };
await getBackendSrv().post(`/api/admin/users/${userId}/revoke-auth-token`, payload);
dispatch(loadUserSessions(userId));
await getBackendSrv().post(`/api/admin/users/${userUid}/revoke-auth-token`, payload);
dispatch(loadUserSessions(userUid));
};
}
export function revokeAllSessions(userId: number): ThunkResult<void> {
export function revokeAllSessions(userUid: string): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().post(`/api/admin/users/${userId}/logout`);
dispatch(loadUserSessions(userId));
await getBackendSrv().post(`/api/admin/users/${userUid}/logout`);
dispatch(loadUserSessions(userUid));
};
}
@ -210,10 +210,10 @@ export function loadLdapSyncStatus(): ThunkResult<void> {
};
}
export function syncLdapUser(userId: number): ThunkResult<void> {
export function syncLdapUser(userId: number, userUid: string): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().post(`/api/admin/ldap/sync/${userId}`);
dispatch(loadAdminUserPage(userId));
dispatch(loadAdminUserPage(userUid));
};
}

View File

@ -56,6 +56,7 @@ const getTestUserMapping = (): LdapUser => ({
const getTestUser = (): UserDTO => ({
id: 1,
uid: 'aaaaaa',
email: 'user@localhost',
login: 'user',
name: 'User',

View File

@ -13,6 +13,7 @@ const defaultProps: Props = {
...initialUserState,
user: {
id: 1,
uid: 'aaaaaa',
name: 'Test User',
email: 'test@test.com',
login: 'test',

View File

@ -31,6 +31,7 @@ const defaultProps: Props = {
...initialUserState,
user: {
id: 1,
uid: 'aaaaaa',
name: 'Test User',
email: 'test@test.com',
login: 'test',

View File

@ -62,6 +62,7 @@ describe('userReducer', () => {
userLoaded({
user: {
id: 2021,
uid: 'aaaaaa',
email: 'test@test.com',
isDisabled: true,
login: 'test',
@ -74,6 +75,7 @@ describe('userReducer', () => {
...initialUserState,
user: {
id: 2021,
uid: 'aaaaaa',
email: 'test@test.com',
isDisabled: true,
login: 'test',

View File

@ -34,6 +34,7 @@ export type Unit = { name: string; url: string };
export interface UserDTO extends WithAccessControlMetadata {
id: number;
uid: string;
login: string;
email: string;
name: string;

View File

@ -2400,6 +2400,9 @@
},
"message": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"type": "object"