From 3d849771bcbc9f81f806f6d0b9a20cb4f00d2582 Mon Sep 17 00:00:00 2001 From: Ilja <ilja@ilja.space> Date: Sat, 10 Sep 2022 12:25:25 +0200 Subject: [PATCH] Add privileges to Moderator dropdown A dropdown in Users and Statuses views. --- .../users/components/ModerationDropdown.vue | 44 ++-- test/views/statuses/statusShowStore.conf.js | 9 +- .../components/ModerationDropdown.test.js | 210 ++++++++++++++++++ test/views/users/components/store.conf.js | 25 +++ test/views/users/store.conf.js | 26 ++- 5 files changed, 291 insertions(+), 23 deletions(-) create mode 100644 test/views/users/components/ModerationDropdown.test.js diff --git a/src/views/users/components/ModerationDropdown.vue b/src/views/users/components/ModerationDropdown.vue index 57023192..913e5245 100644 --- a/src/views/users/components/ModerationDropdown.vue +++ b/src/views/users/components/ModerationDropdown.vue @@ -1,5 +1,5 @@ <template> - <el-dropdown :hide-on-click="false" size="small" trigger="click" placement="top-start" @click.native.stop> + <el-dropdown v-if="isPrivileged(['users_manage_activation_state', 'users_delete', 'users_manage_tags', 'users_manage_credentials'], ['admin'])" :hide-on-click="false" size="small" trigger="click" placement="top-start" @click.native.stop> <div> <el-button v-if="page === 'users'" type="text" class="el-dropdown-link"> {{ $t('users.moderation') }} @@ -17,6 +17,7 @@ </div> <el-dropdown-menu slot="dropdown" class="moderation-dropdown-menu"> <el-dropdown-item + v-if="isPrivileged([], ['admin'])" class="actor-type-dropdown"> <el-select v-model="actorType" :placeholder="$t('userProfile.actorType')" class="actor-type-select"> <el-option :label="$t('users.service')" value="Service"/> @@ -24,51 +25,51 @@ </el-select> </el-dropdown-item> <el-dropdown-item - v-if="showAdminAction(user)" + v-if="isPrivileged([], ['admin']) && showAdminAction(user)" divided @click.native="toggleUserRight(user, 'admin')"> {{ user.roles.admin ? $t('users.revokeAdmin') : $t('users.grantAdmin') }} </el-dropdown-item> <el-dropdown-item - v-if="showAdminAction(user)" + v-if="isPrivileged([], ['admin']) && showAdminAction(user)" @click.native="toggleUserRight(user, 'moderator')"> {{ user.roles.moderator ? $t('users.revokeModerator') : $t('users.grantModerator') }} </el-dropdown-item> <el-dropdown-item - v-if="showDeactivatedButton(user.id) && page !== 'statusPage'" + v-if="isPrivileged(['users_manage_activation_state'], []) && showDeactivatedButton(user.id) && page !== 'statusPage'" :divided="showAdminAction(user)" @click.native="toggleActivation(user)"> {{ !user.is_active ? $t('users.activateAccount') : $t('users.deactivateAccount') }} </el-dropdown-item> <el-dropdown-item - v-if="showDeactivatedButton(user.id) && page !== 'statusPage'" + v-if="isPrivileged(['users_delete'], []) && showDeactivatedButton(user.id) && page !== 'statusPage'" @click.native="handleDeletion(user)"> {{ $t('users.deleteAccount') }} </el-dropdown-item> <el-dropdown-item - v-if="user.local && !user.is_approved" + v-if="isPrivileged([], ['admin']) && user.local && !user.is_approved" divided @click.native="handleAccountApproval(user)"> {{ $t('users.approveAccount') }} </el-dropdown-item> <el-dropdown-item - v-if="user.local && !user.is_approved" + v-if="isPrivileged([], ['admin']) && user.local && !user.is_approved" @click.native="handleAccountRejection(user)"> {{ $t('users.rejectAccount') }} </el-dropdown-item> <el-dropdown-item - v-if="user.local && !user.is_confirmed" + v-if="isPrivileged([], ['admin']) && user.local && !user.is_confirmed" divided @click.native="handleEmailConfirmation(user)"> {{ $t('users.confirmAccount') }} </el-dropdown-item> <el-dropdown-item - v-if="user.local && !user.is_confirmed" + v-if="isPrivileged([], ['admin']) && user.local && !user.is_confirmed" @click.native="handleConfirmationResend(user)"> {{ $t('users.resendConfirmation') }} </el-dropdown-item> <el-dropdown-item - v-if="tagPolicyEnabled" + v-if="isPrivileged(['users_manage_tags'], []) && tagPolicyEnabled" :divided="showAdminAction(user)" :class="{ 'active-tag': user.tags.includes('mrf_tag:media-force-nsfw') }" @click.native="toggleTag(user, 'mrf_tag:media-force-nsfw')"> @@ -76,60 +77,60 @@ <i v-if="user.tags.includes('mrf_tag:media-force-nsfw')" class="el-icon-check"/> </el-dropdown-item> <el-dropdown-item - v-if="tagPolicyEnabled" + v-if="isPrivileged(['users_manage_tags'], []) && tagPolicyEnabled" :class="{ 'active-tag': user.tags.includes('mrf_tag:media-strip') }" @click.native="toggleTag(user, 'mrf_tag:media-strip')"> {{ $t('users.stripMedia') }} <i v-if="user.tags.includes('mrf_tag:media-strip')" class="el-icon-check"/> </el-dropdown-item> <el-dropdown-item - v-if="tagPolicyEnabled" + v-if="isPrivileged(['users_manage_tags'], []) && tagPolicyEnabled" :class="{ 'active-tag': user.tags.includes('mrf_tag:force-unlisted') }" @click.native="toggleTag(user, 'mrf_tag:force-unlisted')"> {{ $t('users.forceUnlisted') }} <i v-if="user.tags.includes('mrf_tag:force-unlisted')" class="el-icon-check"/> </el-dropdown-item> <el-dropdown-item - v-if="tagPolicyEnabled" + v-if="isPrivileged(['users_manage_tags'], []) && tagPolicyEnabled" :class="{ 'active-tag': user.tags.includes('mrf_tag:sandbox') }" @click.native="toggleTag(user, 'mrf_tag:sandbox')"> {{ $t('users.sandbox') }} <i v-if="user.tags.includes('mrf_tag:sandbox')" class="el-icon-check"/> </el-dropdown-item> <el-dropdown-item - v-if="user.local && tagPolicyEnabled" + v-if="isPrivileged(['users_manage_tags'], []) && user.local && tagPolicyEnabled" :class="{ 'active-tag': user.tags.includes('mrf_tag:disable-remote-subscription') }" @click.native="toggleTag(user, 'mrf_tag:disable-remote-subscription')"> {{ $t('users.disableRemoteSubscription') }} <i v-if="user.tags.includes('mrf_tag:disable-remote-subscription')" class="el-icon-check"/> </el-dropdown-item> <el-dropdown-item - v-if="user.local && tagPolicyEnabled" + v-if="isPrivileged(['users_manage_tags'], []) && user.local && tagPolicyEnabled" :class="{ 'active-tag': user.tags.includes('mrf_tag:disable-any-subscription') }" @click.native="toggleTag(user, 'mrf_tag:disable-any-subscription')"> {{ $t('users.disableAnySubscription') }} <i v-if="user.tags.includes('mrf_tag:disable-any-subscription')" class="el-icon-check"/> </el-dropdown-item> <el-dropdown-item - v-if="!tagPolicyEnabled" + v-if="isPrivileged(['users_manage_tags'], []) && isPrivileged([], ['admin']) && !tagPolicyEnabled" divided class="no-hover" @click.native="enableTagPolicy"> {{ $t('users.enableTagPolicy') }} </el-dropdown-item> <el-dropdown-item - v-if="user.local" + v-if="isPrivileged(['users_manage_credentials'], []) && user.local" divided @click.native="getPasswordResetToken(user.nickname)"> {{ $t('users.getPasswordResetToken') }} </el-dropdown-item> <el-dropdown-item - v-if="user.local" + v-if="isPrivileged([], ['admin']) && user.local" @click.native="requirePasswordReset(user)"> {{ $t('users.requirePasswordReset') }} </el-dropdown-item> <el-dropdown-item - v-if="user.local" + v-if="isPrivileged([], ['admin']) && user.local" @click.native="disableMfa(user.nickname)"> {{ $t('users.disableMfa') }} </el-dropdown-item> @@ -181,6 +182,11 @@ export default { disableMfa(nickname) { this.$store.dispatch('DisableMfa', nickname) }, + isPrivileged(accepted_privileges, accepted_roles) { + const user_privileges = this.$store.getters.privileges + const user_roles = this.$store.getters.roles + return accepted_privileges.some(privilege => user_privileges.indexOf(privilege) >= 0) || accepted_roles.some(role => user_roles.indexOf(role) >= 0) + }, enableTagPolicy() { this.$confirm( this.$t('users.confirmEnablingTagPolicy'), diff --git a/test/views/statuses/statusShowStore.conf.js b/test/views/statuses/statusShowStore.conf.js index c83a8660..cc45cd69 100644 --- a/test/views/statuses/statusShowStore.conf.js +++ b/test/views/statuses/statusShowStore.conf.js @@ -13,7 +13,14 @@ export default { peers, settings, status, - user, + user: { + ...user, + state: { + ...user.state, + roles: ['admin'], + privileges: ['users_manage_activation_state', 'users_delete', 'users_manage_tags', 'users_manage_credentials'] + } + }, userProfile, users }, diff --git a/test/views/users/components/ModerationDropdown.test.js b/test/views/users/components/ModerationDropdown.test.js new file mode 100644 index 00000000..7d6013dd --- /dev/null +++ b/test/views/users/components/ModerationDropdown.test.js @@ -0,0 +1,210 @@ +import Vuex from 'vuex' +import { mount, createLocalVue, config, RouterLinkStub } from '@vue/test-utils' +import flushPromises from 'flush-promises' +import Element from 'element-ui' +import Users from '@/views/users/index' +import { + storeNoPrivilegesNoRoles, + storeWithTagPolicyNoPrivilegesRolesAdmin, + storeWithPrivilegesUsersManageActivationStateNoRoles, + storeWithPrivilegesUsersDeleteNoRoles, + storeWithTagPolicyPrivilegesUsersManageTagsNoRoles, + storeWithTagPolicyPrivilegesUsersManageTagsRolesAdmin, + storeWithPrivilegesUsersManageCredentialsNoRoles +} from './store.conf' +import { cloneDeep } from 'lodash' + +config.mocks["$t"] = (key) => key + +const localVue = createLocalVue() +localVue.use(Vuex) +localVue.use(Element) + +jest.mock('@/api/app') +jest.mock('@/api/nodeInfo') +jest.mock('@/api/users') +jest.mock('@/api/settings') + +describe('The Multiple Users Moderation Menu', () => { + it('doesnt show for someone with no privileges and no roles', async (done) => { + const store = new Vuex.Store(cloneDeep(storeNoPrivilegesNoRoles)) + const wrapper = mount(Users, { + store, + localVue, + sync: false, + stubs: { + RouterLink: RouterLinkStub + } + }) + await flushPromises() + + expect(wrapper.find('.moderation-dropdown-menu').exists()).toEqual(false) + done() + }) + + it('shows for someone with admin role and shows proper entries', async (done) => { + const store = new Vuex.Store(cloneDeep(storeWithTagPolicyNoPrivilegesRolesAdmin)) + const wrapper = mount(Users, { + store, + localVue, + sync: false, + stubs: { + RouterLink: RouterLinkStub + } + }) + await flushPromises() + const menu = wrapper.findAll('.moderation-dropdown-menu').at(0) + const menu_items = menu.findAll('.el-dropdown-menu__item') + const menu_items_text = menu_items.wrappers.map(menu_item => menu_item.text()).sort() + + expect(menu.isVisible()).toEqual(true) + expect(menu_items_text).toEqual([ + 'users.disableMfa', + 'users.grantModerator', + 'users.requirePasswordReset', + 'users.revokeAdmin', + 'users.service users.person', + ]) + + store.state.users.mrfPolicies = [] + await flushPromises() + + expect(menu_items.length).toEqual(menu_items_text.length) + done() + }) + + it('shows for someone with users_manage_activation_state privilege and shows proper entries', async (done) => { + const store = new Vuex.Store(cloneDeep(storeWithPrivilegesUsersManageActivationStateNoRoles)) + const wrapper = mount(Users, { + store, + localVue, + sync: false, + stubs: { + RouterLink: RouterLinkStub + } + }) + await flushPromises() + const menu = wrapper.findAll('.moderation-dropdown-menu').at(0) + const menu_items = menu.findAll('.el-dropdown-menu__item') + const menu_items_text = menu_items.wrappers.map(menu_item => menu_item.text()).sort() + + expect(menu.isVisible()).toEqual(true) + expect(menu_items_text).toEqual(['users.deactivateAccount']) + + done() + }) + + it('shows for someone with users_delete privilege and shows proper entries', async (done) => { + const store = new Vuex.Store(cloneDeep(storeWithPrivilegesUsersDeleteNoRoles)) + const wrapper = mount(Users, { + store, + localVue, + sync: false, + stubs: { + RouterLink: RouterLinkStub + } + }) + await flushPromises() + const menu = wrapper.findAll('.moderation-dropdown-menu').at(0) + const menu_items = menu.findAll('.el-dropdown-menu__item') + const menu_items_text = menu_items.wrappers.map(menu_item => menu_item.text()).sort() + + expect(menu.isVisible()).toEqual(true) + expect(menu_items_text).toEqual(['users.deleteAccount']) + + done() + }) + + it('shows for someone with users_manage_tags privilege and shows proper entries depending on wether tagpolicy is set', async (done) => { + const store = new Vuex.Store(cloneDeep(storeWithTagPolicyPrivilegesUsersManageTagsNoRoles)) + const wrapper = mount(Users, { + store, + localVue, + sync: false, + stubs: { + RouterLink: RouterLinkStub + } + }) + await flushPromises() + const menu = wrapper.findAll('.moderation-dropdown-menu').at(0) + const menu_items = menu.findAll('.el-dropdown-menu__item') + const menu_items_text = menu_items.wrappers.map(menu_item => menu_item.text()).sort() + + expect(menu.isVisible()).toEqual(true) + expect(menu_items_text).toEqual([ + 'users.disableAnySubscription', + 'users.disableRemoteSubscription', + 'users.forceNsfw', + 'users.forceUnlisted', + 'users.sandbox', + 'users.stripMedia' + ]) + + store.state.users.mrfPolicies = [] + await flushPromises() + + expect(menu.findAll('.el-dropdown-menu__item').length).toEqual(0) + + done() + }) + + it('shows enable tagpolicy for someone with users_manage_tags privilege and admin role when tagpolicy is not set', async (done) => { + const store = new Vuex.Store(cloneDeep(storeWithTagPolicyPrivilegesUsersManageTagsRolesAdmin)) + const wrapper = mount(Users, { + store, + localVue, + sync: false, + stubs: { + RouterLink: RouterLinkStub + } + }) + await flushPromises() + const menu = wrapper.findAll('.moderation-dropdown-menu').at(0) + const menu_items = menu.findAll('.el-dropdown-menu__item') + const menu_items_text = menu_items.wrappers.map(menu_item => menu_item.text()) + + expect(menu.isVisible()).toEqual(true) + expect(menu_items_text.includes('users.disableAnySubscription')).toBe(true) + expect(menu_items_text.includes('users.disableRemoteSubscription')).toBe(true) + expect(menu_items_text.includes('users.forceNsfw')).toBe(true) + expect(menu_items_text.includes('users.forceUnlisted')).toBe(true) + expect(menu_items_text.includes('users.sandbox')).toBe(true) + expect(menu_items_text.includes('users.stripMedia')).toBe(true) + + store.state.users.mrfPolicies = [] + await flushPromises() + + const menu_items_text_no_policy = menu.findAll('.el-dropdown-menu__item').wrappers.map(menu_item => menu_item.text()) + expect(menu_items_text_no_policy.includes('users.disableAnySubscription')).toBe(false) + expect(menu_items_text_no_policy.includes('users.disableRemoteSubscription')).toBe(false) + expect(menu_items_text_no_policy.includes('users.forceNsfw')).toBe(false) + expect(menu_items_text_no_policy.includes('users.forceUnlisted')).toBe(false) + expect(menu_items_text_no_policy.includes('users.sandbox')).toBe(false) + expect(menu_items_text_no_policy.includes('users.stripMedia')).toBe(false) + + expect(menu_items_text_no_policy.includes('users.enableTagPolicy')).toBe(true) + + done() + }) + + it('shows for someone with users_manage_credentials privilege and shows proper entries', async (done) => { + const store = new Vuex.Store(cloneDeep(storeWithPrivilegesUsersManageCredentialsNoRoles)) + const wrapper = mount(Users, { + store, + localVue, + sync: false, + stubs: { + RouterLink: RouterLinkStub + } + }) + await flushPromises() + const menu = wrapper.findAll('.moderation-dropdown-menu').at(0) + const menu_items = menu.findAll('.el-dropdown-menu__item') + const menu_items_text = menu_items.wrappers.map(menu_item => menu_item.text()).sort() + + expect(menu.isVisible()).toEqual(true) + expect(menu_items_text).toEqual(['users.getPasswordResetToken']) + + done() + }) +}) diff --git a/test/views/users/components/store.conf.js b/test/views/users/components/store.conf.js index e0f7d2bf..1a084353 100644 --- a/test/views/users/components/store.conf.js +++ b/test/views/users/components/store.conf.js @@ -12,6 +12,7 @@ export const storeNoPrivilegesNoRoles = { user: { ...user, state: { + ...user.state, roles: [], privileges: [] } @@ -29,6 +30,7 @@ export const storeWithTagPolicyNoPrivilegesRolesAdmin = { user: { ...user, state: { + ...user.state, roles: ['admin'], privileges: [] } @@ -46,6 +48,7 @@ export const storeWithPrivilegesUsersManageInvitesNoRoles = { user: { ...user, state: { + ...user.state, roles: [], privileges: ['users_manage_invites'] } @@ -63,6 +66,7 @@ export const storeWithPrivilegesUsersDeleteNoRoles = { user: { ...user, state: { + ...user.state, roles: [], privileges: ['users_delete'] } @@ -80,6 +84,7 @@ export const storeWithPrivilegesUsersManageActivationStateNoRoles = { user: { ...user, state: { + ...user.state, roles: [], privileges: ['users_manage_activation_state'] } @@ -97,6 +102,7 @@ export const storeWithTagPolicyPrivilegesUsersManageTagsNoRoles = { user: { ...user, state: { + ...user.state, roles: [], privileges: ['users_manage_tags'] } @@ -114,6 +120,7 @@ export const storeWithTagPolicyPrivilegesUsersManageTagsRolesAdmin = { user: { ...user, state: { + ...user.state, roles: ['admin'], privileges: ['users_manage_tags'] } @@ -123,3 +130,21 @@ export const storeWithTagPolicyPrivilegesUsersManageTagsRolesAdmin = { }, getters } + +export const storeWithPrivilegesUsersManageCredentialsNoRoles = { + modules: { + app, + settings, + user: { + ...user, + state: { + ...user.state, + roles: [], + privileges: ['users_manage_credentials'] + } + }, + userProfile, + users + }, + getters +} diff --git a/test/views/users/store.conf.js b/test/views/users/store.conf.js index 7edf2865..265ebbfb 100644 --- a/test/views/users/store.conf.js +++ b/test/views/users/store.conf.js @@ -9,7 +9,14 @@ export const storeConfig = { modules: { app, settings, - user, + user: { + ...user, + state: { + ...user.state, + roles: ['admin'], + privileges: ['users_manage_activation_state', 'users_delete', 'users_manage_tags', 'users_manage_credentials'] + } + }, userProfile, users }, @@ -20,9 +27,22 @@ export const storeWithTagPolicy = { modules: { app, settings, - user, + user: { + ...user, + state: { + ...user.state, + roles: ['admin'], + privileges: ['users_manage_activation_state', 'users_delete', 'users_manage_tags', 'users_manage_credentials'] + } + }, userProfile, - users: { ...users, state: { ...users.state, mrfPolicies: ['Pleroma.Web.ActivityPub.MRF.TagPolicy'] }} + users: { + ...users, + state: { + ...users.state, + mrfPolicies: ['Pleroma.Web.ActivityPub.MRF.TagPolicy'] + } + } }, getters } -- GitLab