diff --git a/pkg/services/org/model.go b/pkg/services/org/model.go index 9e2691f0340..7873e899eb3 100644 --- a/pkg/services/org/model.go +++ b/pkg/services/org/model.go @@ -157,6 +157,7 @@ type OrgUserDTO struct { IsDisabled bool `json:"isDisabled"` AuthLabels []string `json:"authLabels" xorm:"-"` IsExternallySynced bool `json:"isExternallySynced"` + IsProvisioned bool `json:"isProvisioned"` } type RemoveOrgUserCommand struct { diff --git a/pkg/services/org/orgimpl/store.go b/pkg/services/org/orgimpl/store.go index 04edfb1e8f0..be838861c6a 100644 --- a/pkg/services/org/orgimpl/store.go +++ b/pkg/services/org/orgimpl/store.go @@ -611,6 +611,7 @@ func (ss *sqlStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUser "u.created", "u.updated", "u.is_disabled", + "u.is_provisioned", ) if len(query.SortOpts) > 0 { diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go index 78d9f492e36..6e2377052a9 100644 --- a/pkg/services/user/model.go +++ b/pkg/services/user/model.go @@ -137,11 +137,11 @@ type UserSearchHitDTO struct { AvatarURL string `json:"avatarUrl" xorm:"avatar_url"` IsAdmin bool `json:"isAdmin"` IsDisabled bool `json:"isDisabled"` + IsProvisioned bool `json:"isProvisioned"` LastSeenAt time.Time `json:"lastSeenAt"` LastSeenAtAge string `json:"lastSeenAtAge"` AuthLabels []string `json:"authLabels"` AuthModule AuthModuleConversion `json:"-"` - IsProvisioned bool `json:"-" xorm:"is_provisioned"` } type GetUserProfileQuery struct { @@ -166,7 +166,7 @@ type UserProfileDTO struct { CreatedAt time.Time `json:"createdAt"` AvatarURL string `json:"avatarUrl"` AccessControl map[string]bool `json:"accessControl,omitempty"` - IsProvisioned bool `json:"-"` + IsProvisioned bool `json:"isProvisioned"` } // implement Conversion interface to define custom field mapping (xorm feature) diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index f740949942e..eb6b5a68227 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -374,6 +374,7 @@ func (ss *sqlStore) GetProfile(ctx context.Context, query *user.GetUserProfileQu Theme: usr.Theme, IsGrafanaAdmin: usr.IsAdmin, IsDisabled: usr.IsDisabled, + IsProvisioned: usr.IsProvisioned, OrgID: usr.OrgID, UpdatedAt: usr.Updated, CreatedAt: usr.Created, @@ -526,7 +527,7 @@ func (ss *sqlStore) Search(ctx context.Context, query *user.SearchUsersQuery) (* sess.Limit(query.Limit, offset) } - sess.Cols("u.id", "u.uid", "u.email", "u.name", "u.login", "u.is_admin", "u.is_disabled", "u.last_seen_at", "user_auth.auth_module") + sess.Cols("u.id", "u.uid", "u.email", "u.name", "u.login", "u.is_admin", "u.is_disabled", "u.last_seen_at", "user_auth.auth_module", "u.is_provisioned") if len(query.SortOpts) > 0 { for i := range query.SortOpts { diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index a036e242e30..cc8046467ec 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5913,6 +5913,9 @@ "isExternallySynced": { "type": "boolean" }, + "isProvisioned": { + "type": "boolean" + }, "lastSeenAt": { "type": "string", "format": "date-time" @@ -8605,6 +8608,9 @@ "isGrafanaAdminExternallySynced": { "type": "boolean" }, + "isProvisioned": { + "type": "boolean" + }, "login": { "type": "string" }, @@ -8652,6 +8658,9 @@ "isDisabled": { "type": "boolean" }, + "isProvisioned": { + "type": "boolean" + }, "lastSeenAt": { "type": "string", "format": "date-time" diff --git a/public/api-merged.json b/public/api-merged.json index 3ca4e15b213..cba17c0c8ac 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -17760,6 +17760,9 @@ "isExternallySynced": { "type": "boolean" }, + "isProvisioned": { + "type": "boolean" + }, "lastSeenAt": { "type": "string", "format": "date-time" @@ -22367,6 +22370,9 @@ "isGrafanaAdminExternallySynced": { "type": "boolean" }, + "isProvisioned": { + "type": "boolean" + }, "login": { "type": "string" }, @@ -22414,6 +22420,9 @@ "isDisabled": { "type": "boolean" }, + "isProvisioned": { + "type": "boolean" + }, "lastSeenAt": { "type": "string", "format": "date-time" diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index 46d6fa41f03..9af0af250b3 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -115,7 +115,10 @@ export const UserAdminPage = ({ const isLDAPUser = user?.isExternal && user?.authLabels?.includes('LDAP'); const canReadSessions = contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList); const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead); - const authSource = user?.authLabels?.[0]; + let authSource = user?.authLabels?.[0]; + if (user?.isProvisioned) { + authSource = 'SCIM'; + } const lockMessage = authSource ? `Synced via ${authSource}` : ''; const pageNav: NavModelItem = { text: user?.login ?? '', diff --git a/public/app/features/admin/UserProfile.tsx b/public/app/features/admin/UserProfile.tsx index 01f4256d523..8fa8e1f0bc4 100644 --- a/public/app/features/admin/UserProfile.tsx +++ b/public/app/features/admin/UserProfile.tsx @@ -71,12 +71,18 @@ export function UserProfile({ }); }; - const authSource = user.authLabels?.length && user.authLabels[0]; + let authSource = user.authLabels?.length && user.authLabels[0]; + if (user.isProvisioned) { + authSource = 'SCIM'; + } const lockMessage = authSource ? `Synced via ${authSource}` : ''; - const editLocked = user.isExternal || !contextSrv.hasPermissionInMetadata(AccessControlAction.UsersWrite, user); + const editLocked = + user.isExternal || user.isProvisioned || !contextSrv.hasPermissionInMetadata(AccessControlAction.UsersWrite, user); const passwordChangeLocked = - user.isExternal || !contextSrv.hasPermissionInMetadata(AccessControlAction.UsersPasswordUpdate, user); + user.isExternal || + user.isProvisioned || + !contextSrv.hasPermissionInMetadata(AccessControlAction.UsersPasswordUpdate, user); const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersDelete, user); const canDisable = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersDisable, user); const canEnable = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersEnable, user); diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx index 861181db16c..9e27806a05c 100644 --- a/public/app/features/admin/Users/OrgUsersTable.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.tsx @@ -216,6 +216,13 @@ export const OrgUsersTable = ({ <>{Array.isArray(value) && value.length > 0 && } ), }, + { + id: 'isProvisioned', + header: 'Provisioned', + cell: ({ cell: { value } }: Cell<'isProvisioned'>) => ( + <>{value && } + ), + }, { id: 'isDisabled', header: '', diff --git a/public/app/features/admin/Users/UsersTable.tsx b/public/app/features/admin/Users/UsersTable.tsx index 23fbf4b763f..405186e229b 100644 --- a/public/app/features/admin/Users/UsersTable.tsx +++ b/public/app/features/admin/Users/UsersTable.tsx @@ -152,6 +152,13 @@ export const UsersTable = ({ <>{Array.isArray(value) && value.length > 0 && } ), }, + { + id: 'isProvisioned', + header: 'Provisioned', + cell: ({ cell: { value } }: Cell<'isProvisioned'>) => ( + <>{value && } + ), + }, { id: 'isDisabled', header: '', diff --git a/public/app/features/profile/UserProfileEditForm.tsx b/public/app/features/profile/UserProfileEditForm.tsx index f942b6876fc..0c788577731 100644 --- a/public/app/features/profile/UserProfileEditForm.tsx +++ b/public/app/features/profile/UserProfileEditForm.tsx @@ -22,7 +22,10 @@ export const UserProfileEditForm = ({ user, isSavingUser, updateProfile }: Props // check if authLabels is longer than 0 otherwise false const isExternalUser: boolean = (user && user.isExternal) ?? false; - const authSource = isExternalUser && user && user.authLabels ? user.authLabels[0] : ''; + let authSource = isExternalUser && user && user.authLabels ? user.authLabels[0] : ''; + if (user?.isProvisioned) { + authSource = 'SCIM'; + } const lockMessage = authSource ? ` (Synced via ${authSource})` : ''; const disabledEdit = disableLoginForm || isExternalUser; diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 0d6a231692c..67264b0f298 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -19,6 +19,8 @@ export interface OrgUser extends WithAccessControlMetadata { isDisabled: boolean; authLabels?: string[]; isExternallySynced?: boolean; + // Externally provisioned + isProvisioned?: boolean; } export interface User { @@ -56,6 +58,7 @@ export interface UserDTO extends WithAccessControlMetadata { orgs?: Unit[]; isExternallySynced?: boolean; isGrafanaAdminExternallySynced?: boolean; + isProvisioned?: boolean; } export interface Invitee { diff --git a/public/openapi3.json b/public/openapi3.json index d89f792df71..78d904c2847 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -7822,6 +7822,9 @@ "isExternallySynced": { "type": "boolean" }, + "isProvisioned": { + "type": "boolean" + }, "lastSeenAt": { "format": "date-time", "type": "string" @@ -12428,6 +12431,9 @@ "isGrafanaAdminExternallySynced": { "type": "boolean" }, + "isProvisioned": { + "type": "boolean" + }, "login": { "type": "string" }, @@ -12475,6 +12481,9 @@ "isDisabled": { "type": "boolean" }, + "isProvisioned": { + "type": "boolean" + }, "lastSeenAt": { "format": "date-time", "type": "string"