feat: Add user role select to users list settings page (#7796)

![CleanShot 2023-11-27 at 07 20
58](https://github.com/n8n-io/n8n/assets/5410822/40be0505-32ee-48a7-923e-ba6b4dbce670)
This commit is contained in:
Csaba Tuncsik 2023-11-27 13:38:03 +01:00 committed by GitHub
parent 5acb7b94c0
commit 137e23853f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 74 additions and 17 deletions

View file

@ -35,10 +35,11 @@ export type Scope =
| SourceControlScope
| ExternalSecretStoreScope;
export type ScopeLevel<T extends 'global' | 'project' | 'resource'> = Record<T, Scope[]>;
export type GlobalScopes = ScopeLevel<'global'>;
export type ProjectScopes = ScopeLevel<'project'>;
export type ResourceScopes = ScopeLevel<'resource'>;
export type ScopeLevel = 'global' | 'project' | 'resource';
export type GetScopeLevel<T extends ScopeLevel> = Record<T, Scope[]>;
export type GlobalScopes = GetScopeLevel<'global'>;
export type ProjectScopes = GetScopeLevel<'project'>;
export type ResourceScopes = GetScopeLevel<'resource'>;
export type ScopeLevels = GlobalScopes & (ProjectScopes | (ProjectScopes & ResourceScopes));
export type ScopeMode = 'oneOf' | 'allOf';

View file

@ -670,7 +670,7 @@ export type IPersonalizationSurveyVersions =
| IPersonalizationSurveyAnswersV2
| IPersonalizationSurveyAnswersV3;
export type IRole = 'default' | 'owner' | 'member';
export type IRole = 'default' | 'owner' | 'member' | 'admin';
export interface IUserResponse {
id: string;

View file

@ -2,10 +2,12 @@ import type {
CurrentUserResponse,
IPersonalizationLatestVersion,
IRestApiContext,
IRole,
IUserResponse,
} from '@/Interface';
import type { IDataObject } from 'n8n-workflow';
import { makeRestApiRequest } from '@/utils/apiUtils';
import type { ScopeLevel } from '@n8n/permissions';
export async function loginCurrentUser(
context: IRestApiContext,
@ -143,3 +145,10 @@ export async function submitPersonalizationSurvey(
): Promise<void> {
await makeRestApiRequest(context, 'POST', '/me/survey', params as unknown as IDataObject);
}
export async function updateRole(
context: IRestApiContext,
{ id, role }: { id: string; role: { scope: ScopeLevel; name: IRole } },
): Promise<IUserResponse> {
return makeRestApiRequest(context, 'PATCH', `/users/${id}/role`, { newRole: role });
}

View file

@ -132,6 +132,10 @@ export default defineComponent({
value: ROLE.Member,
label: this.$locale.baseText('auth.roles.member'),
},
{
value: ROLE.Admin,
label: this.$locale.baseText('auth.roles.admin'),
},
],
capitalize: true,
},

View file

@ -103,6 +103,7 @@
"auth.password": "Password",
"auth.role": "Role",
"auth.roles.member": "Member",
"auth.roles.admin": "Admin",
"auth.roles.owner": "Owner",
"auth.agreement.label": "Inform me about security vulnerabilities if they arise",
"auth.setup.next": "Next",

View file

@ -476,9 +476,11 @@ export const routes = [
settingsView: SettingsUsersView,
},
meta: {
middleware: ['authenticated', 'role'],
middleware: ['authenticated', 'rbac'],
middlewareOptions: {
role: [ROLE.Owner],
rbac: {
scope: ['user:create', 'user:update'],
},
},
telemetry: {
pageCategory: 'settings',

View file

@ -15,6 +15,7 @@ import {
updateOtherUserSettings,
validatePasswordToken,
validateSignupToken,
updateRole,
} from '@/api/users';
import { PERSONALIZATION_MODAL_KEY, STORES } from '@/constants';
import type {
@ -39,16 +40,13 @@ import { useCloudPlanStore } from './cloudPlan.store';
import { disableMfa, enableMfa, getMfaQR, verifyMfaToken } from '@/api/mfa';
import { confirmEmail, getCloudUserInfo } from '@/api/cloudPlans';
import { useRBACStore } from '@/stores/rbac.store';
import type { Scope } from '@n8n/permissions';
import type { Scope, ScopeLevel } from '@n8n/permissions';
import { inviteUsers, acceptInvitation } from '@/api/invitation';
const isDefaultUser = (user: IUserResponse | null) =>
Boolean(user && user.isPending && user.globalRole && user.globalRole.name === ROLE.Owner);
const isPendingUser = (user: IUserResponse | null) => Boolean(user && user.isPending);
const isInstanceOwner = (user: IUserResponse | null) =>
Boolean(user?.globalRole?.name === ROLE.Owner);
user?.isPending && user?.globalRole?.name === ROLE.Owner;
const isPendingUser = (user: IUserResponse | null) => !!user?.isPending;
const isInstanceOwner = (user: IUserResponse | null) => user?.globalRole?.name === ROLE.Owner;
export const useUsersStore = defineStore(STORES.USERS, {
state: (): IUsersState => ({
@ -375,5 +373,11 @@ export const useUsersStore = defineStore(STORES.USERS, {
async confirmEmail() {
await confirmEmail(useRootStore().getRestApiContext);
},
async updateRole({ id, role }: { id: string; role: { scope: ScopeLevel; name: IRole } }) {
const rootStore = useRootStore();
await updateRole(rootStore.getRestApiContext, { id, role });
await this.fetchUsers();
},
},
});

View file

@ -84,9 +84,11 @@ function isPersonalizationSurveyV2OrLater(
return 'version' in data;
}
export const ROLE: { Owner: IRole; Member: IRole; Default: IRole } = {
export type Roles = { [R in IRole as Capitalize<R>]: R };
export const ROLE: Roles = {
Owner: 'owner',
Member: 'member',
Admin: 'admin',
Default: 'default', // default user with no email when setting up instance
};

View file

@ -52,7 +52,23 @@
@copyPasswordResetLink="onCopyPasswordResetLink"
@allowSSOManualLogin="onAllowSSOManualLogin"
@disallowSSOManualLogin="onDisallowSSOManualLogin"
/>
>
<template #actions="{ user }">
<n8n-select
:modelValue="user.globalRole.name"
@update:modelValue="($event: IRole) => onRoleChange(user, $event)"
:disabled="!canUpdateRole"
data-test-id="user-role-select"
>
<n8n-option
v-for="role in userRoles"
:key="role.value"
:value="role.value"
:label="role.label"
/>
</n8n-select>
</template>
</n8n-users-list>
</div>
</div>
</template>
@ -62,7 +78,7 @@ import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY, VIEWS } from '@/constants';
import type { IUserListAction } from '@/Interface';
import type { IRole, IUser, IUserListAction } from '@/Interface';
import { useToast } from '@/composables';
import { copyPaste } from '@/mixins/copyPaste';
import { useUIStore } from '@/stores/ui.store';
@ -133,6 +149,21 @@ export default defineComponent({
},
];
},
userRoles(): Array<{ value: IRole; label: string }> {
return [
{
value: ROLE.Member,
label: this.$locale.baseText('auth.roles.member'),
},
{
value: ROLE.Admin,
label: this.$locale.baseText('auth.roles.admin'),
},
];
},
canUpdateRole(): boolean {
return hasPermission(['rbac'], { rbac: { scope: 'user:update' } });
},
},
methods: {
redirectToSetup() {
@ -220,6 +251,9 @@ export default defineComponent({
goToUpgrade() {
void this.uiStore.goToUpgrade('settings-users', 'upgrade-users');
},
async onRoleChange(user: IUser, name: IRole) {
await this.usersStore.updateRole({ id: user.id, role: { scope: 'global', name } });
},
},
});
</script>