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