Skip to content

Commit

Permalink
Merge pull request #1004 from flowforge/add-hasperms
Browse files Browse the repository at this point in the history
Introduce hasPermission mixin for checking rbac in frontend
  • Loading branch information
hardillb committed Sep 22, 2022
2 parents 08e59b0 + f2b060b commit d49255b
Show file tree
Hide file tree
Showing 24 changed files with 184 additions and 160 deletions.
49 changes: 49 additions & 0 deletions forge/lib/permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { Roles } = require('./roles.js')

module.exports = {
Permissions: {
// Team Scoped Actions
'team:create': { description: 'Create Team', admin: false },
'team:edit': { description: 'Edit Team', role: Roles.Owner },
'team:delete': { description: 'Delete Team', role: Roles.Owner },
'team:audit-log': { description: 'Access Team Audit Log', role: Roles.Owner },
// Team Members
'team:user:add': { description: 'Add Members', role: Roles.Admin },
'team:user:invite': { description: 'Invite Members', role: Roles.Owner },
'team:user:remove': { description: 'Remove Member', role: Roles.Owner, self: true },
'team:user:change-role': { description: 'Modify Member role', role: Roles.Owner },
// Projects
'project:create': { description: 'Create Project', role: Roles.Owner },
'project:delete': { description: 'Delete Project', role: Roles.Owner },
'project:transfer': { description: 'Transfer Project', role: Roles.Owner },
'project:change-status': { description: 'Start/Stop Project', role: Roles.Owner },
'project:edit': { description: 'Edit Project Settings', role: Roles.Owner },
'project:edit-env': { description: 'Edit Project Environment Variables', role: Roles.Member },
'project:log': { description: 'Access Project Log', role: Roles.Member },
'project:audit-log': { description: 'Access Project Audit Log', role: Roles.Member },
// Project Editor
'project:flows:view': { description: 'View Project Flows', role: Roles.Member },
'project:flows:edit': { description: 'Edit Project Flows', role: Roles.Member },
'project:snapshot:create': { description: 'Create Project Snapshot', role: Roles.Member },
'project:snapshot:delete': { description: 'Delete Project Snapshot', role: Roles.Owner },
'project:snapshot:rollback': { description: 'Rollback Project Snapshot', role: Roles.Member },
// Templates
'template:create': { description: 'Create a Template', role: Roles.Admin },
'template:delete': { description: 'Delete a Template', role: Roles.Admin },
'template:edit': { description: 'Edit a Template', role: Roles.Admin },
// Stacks
'stack:create': { description: 'Create a Stack', role: Roles.Admin },
'stack:delete': { description: 'Delete a Stack', role: Roles.Admin },
'stack:edit': { description: 'Edit a Stack', role: Roles.Admin },
// Devices
'device:list': { description: 'List Devices', role: Roles.Admin },
'device:create': { description: 'Create a Device', role: Roles.Owner },
'device:delete': { description: 'Delete a Device', role: Roles.Owner },
'device:edit': { description: 'Edit a Device', role: Roles.Owner },
'device:edit-env': { description: 'Edit Device Environment Variables', role: Roles.Member },
// Project Types
'project-type:create': { description: 'Create a ProjectType', role: Roles.Admin },
'project-type:delete': { description: 'Delete a ProjectType', role: Roles.Admin },
'project-type:edit': { description: 'Edit a ProjectType', role: Roles.Admin }
}
}
3 changes: 1 addition & 2 deletions forge/routes/api/projectActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
*/
module.exports = async function (app) {
const changeStatusPreHandler = { preHandler: app.needsPermission('project:change-status') }
const rollbackPreHandler = { preHandler: app.needsPermission('project:rollback') }
app.post('/start', changeStatusPreHandler, async (request, reply) => {
try {
if (request.project.state === 'suspended') {
Expand Down Expand Up @@ -109,7 +108,7 @@ module.exports = async function (app) {
}
})

app.post('/rollback', rollbackPreHandler, async (request, reply) => {
app.post('/rollback', { preHandler: app.needsPermission('project:snapshot:rollback') }, async (request, reply) => {
let restartProject = false
try {
// get (and check) snapshot is valid / owned by project before any actions
Expand Down
53 changes: 3 additions & 50 deletions forge/routes/auth/permissions.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,9 @@
const fp = require('fastify-plugin')
const { Roles } = require('../../lib/roles.js')

const defaultPermissions = {
// Team Scoped Actions
'team:create': { description: 'Create Team', admin: false },
'team:edit': { description: 'Edit Team', role: Roles.Owner },
'team:delete': { description: 'Delete Team', role: Roles.Owner },
'team:audit-log': { description: 'Access Team Audit Log', role: Roles.Owner },
// Team Members
'team:user:add': { description: 'Add Members', role: Roles.Admin },
'team:user:invite': { description: 'Invite Members', role: Roles.Owner },
'team:user:remove': { description: 'Remove Member', role: Roles.Owner, self: true },
'team:user:change-role': { description: 'Modify Member role', role: Roles.Owner },
// Projects
'project:create': { description: 'Create Project', role: Roles.Owner },
'project:delete': { description: 'Delete Project', role: Roles.Owner },
'project:transfer': { description: 'Transfer Project', role: Roles.Owner },
'project:change-status': { description: 'Start/Stop Project', role: Roles.Owner },
'project:rollback': { description: 'Start/Stop Project', role: Roles.Member },
'project:edit': { description: 'Edit Project Settings', role: Roles.Owner },
'project:edit-env': { description: 'Edit Project Environment Variables', role: Roles.Member },
'project:log': { description: 'Access Project Log', role: Roles.Member },
'project:audit-log': { description: 'Access Project Audit Log', role: Roles.Member },
// Project Editor
'project:flows:view': { description: 'View Project Flows', role: Roles.Member },
'project:flows:edit': { description: 'Edit Project Flows', role: Roles.Member },
'project:snapshot:create': { description: 'Create Project Snapshot', role: Roles.Member },
'project:snapshot:delete': { description: 'Delete Project Snapshot', role: Roles.Owner },
// Templates
'template:create': { description: 'Create a Template', role: Roles.Admin },
'template:delete': { description: 'Delete a Template', role: Roles.Admin },
'template:edit': { description: 'Edit a Template', role: Roles.Admin },
// Stacks
'stack:create': { description: 'Create a Stack', role: Roles.Admin },
'stack:delete': { description: 'Delete a Stack', role: Roles.Admin },
'stack:edit': { description: 'Edit a Stack', role: Roles.Admin },
// Devices
'device:list': { description: 'List Devices', role: Roles.Admin },
'device:create': { description: 'Create a Device', role: Roles.Owner },
'device:delete': { description: 'Delete a Device', role: Roles.Owner },
'device:edit': { description: 'Edit a Device', role: Roles.Owner },
'device:edit-env': { description: 'Edit Device Environment Variables', role: Roles.Member },
// Project Types
'project-type:create': { description: 'Create a ProjectType', role: Roles.Admin },
'project-type:delete': { description: 'Delete a ProjectType', role: Roles.Admin },
'project-type:edit': { description: 'Edit a ProjectType', role: Roles.Admin }

}
const { Permissions } = require('../../lib/permissions')

module.exports = fp(async function (app, opts, done) {
function needsPermission (scope) {
if (!defaultPermissions[scope]) {
if (!Permissions[scope]) {
throw new Error(`Unrecognised scope requested: '${scope}'`)
}
return async (request, reply) => {
Expand All @@ -65,7 +18,7 @@ module.exports = fp(async function (app, opts, done) {

// For all Team based permissions, the request should already have
// request.team and request.teamMembership set
const permission = defaultPermissions[scope]
const permission = Permissions[scope]
if (permission.admin) {
// Requires admin user - which would have already been approved
// if they were an admin
Expand Down
10 changes: 4 additions & 6 deletions frontend/src/components/SideNavigationTeamOptions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
<nav-item :label="route.label" :icon="route.icon"></nav-item>
</router-link>
</ul>
<span v-if="showAdmin" class="ff-navigation-divider">
<span v-if="hasPermission('team:edit')" class="ff-navigation-divider">
<label>Team Admin Zone</label>
</span>
<!-- Team Options: Admin -->
<ul v-if="showAdmin" class="ff-side-navigation--admin">
<ul v-if="hasPermission('team:edit')" class="ff-side-navigation--admin">
<router-link v-for="route in routes.admin" :key="route.label"
:to="'/team/' + team.slug + route.to"
:data-nav="route.tag">
Expand All @@ -31,7 +31,7 @@
<script>
import { mapState } from 'vuex'
import { Roles } from '@core/lib/roles'
import permissionsMixin from '@/mixins/Permissions'
import ProjectsIcon from '@/components/icons/Projects'
import { ChipIcon, UsersIcon, DatabaseIcon, TemplateIcon, CurrencyDollarIcon, CogIcon } from '@heroicons/vue/solid'
Expand All @@ -41,14 +41,12 @@ export default {
name: 'FFSideNavigationTeamOptions',
props: ['mobile-menu-open'],
emits: ['option-selected'],
mixins: [permissionsMixin],
components: {
NavItem
},
computed: {
...mapState('account', ['user', 'team', 'teamMembership', 'features', 'notifications']),
showAdmin: function () {
return this.teamMembership.role >= Roles.Owner
},
nested: function () {
return (this.$slots['nested-menu'] && this.loaded) || this.closeNested
}
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/mixins/Permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Permissions } from '@core/lib/permissions'

export default {
methods: {
hasPermission (scope) {
if (!Permissions[scope]) {
throw new Error(`Unrecognised scope requested: '${scope}'`)
}
const permission = Permissions[scope]
// if (<check settings>) {
if (permission.role) {
if (!this.teamMembership) {
return false
}
if (this.teamMembership.role < permission.role) {
return false
}
}
return true
}
}
}
14 changes: 7 additions & 7 deletions frontend/src/pages/admin/Template/sections/Environment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,28 @@
v-model="item.name"
:error="item.error"
:disabled="item.encrypted"
:type="(editTemplate || item.policy === undefined)?'text':'uneditable'"></FormRow>
:type="(!readOnly && (editTemplate || item.policy === undefined))?'text':'uneditable'"></FormRow>
<!-- <ff-text-input v-model="item.name" :disabled="item.encrypted" /> -->
</td>
<td class="px-4 py-4 border w-auto align-top" :class="{'align-middle':item.encrypted}">
<div class="w-full" v-if="!item.encrypted">
<FormRow
class="font-mono"
v-model="item.value"
:type="(editTemplate || item.policy === undefined || item.policy)?'text':'uneditable'"></FormRow>
:type="(!readOnly && (editTemplate || item.policy === undefined || item.policy))?'text':'uneditable'"></FormRow>
</div>
<div v-else class="pt-1 text-gray-400"><LockClosedIcon class="inline w-4" /> encrypted</div>
</td>
<td class="border w-16 align-middle">
<div v-if="(editTemplate|| item.policy === undefined )" class="flex justify-center ">
<div v-if="(!readOnly && (editTemplate|| item.policy === undefined))" class="flex justify-center ">
<ff-button kind="tertiary" @click="removeEnv(itemIdx)" size="small">
<template v-slot:icon>
<TrashIcon />
</template>
</ff-button>
</div>
</td>
<td v-if="editTemplate" class="px-4 py-4 align-middle">
<td v-if="!readOnly && editTemplate" class="px-4 py-4 align-middle">
<LockSetting :editTemplate="editTemplate" v-model="item.policy"></LockSetting>
</td>
</tr>
Expand All @@ -54,10 +54,10 @@
</td>
</tr>
<!-- Empty row to differentiate between the existing env vars, and the iput form row-->
<tr>
<tr v-if="!readOnly">
<td :colspan="editTemplate?4:3" class="p-4 bg-gray-50"></td>
</tr>
<tr class="">
<tr v-if="!readOnly" class="">
<td class="px-4 pt-4 border w-auto align-top">
<FormRow class="font-mono" v-model="input.name" :error="input.error" placeholder="Name"></FormRow>
</td>
Expand Down Expand Up @@ -92,7 +92,7 @@ import { TrashIcon, PlusSmIcon, LockClosedIcon } from '@heroicons/vue/outline'
export default {
name: 'TemplateEnvironmentEditor',
props: ['editTemplate', 'modelValue'],
props: ['editTemplate', 'modelValue', 'readOnly'],
emits: ['update:modelValue'],
data () {
return {
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/pages/device/Settings/Danger.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
<script>
import { mapState } from 'vuex'
import { useRouter } from 'vue-router'
import { Roles } from '@core/lib/roles'
import deviceApi from '@/api/devices'
import permissionsMixin from '@/mixins/Permissions'
import FormHeading from '@/components/FormHeading'
import ConfirmDeviceDeleteDialog from './dialogs/ConfirmDeviceDeleteDialog'
Expand All @@ -27,6 +27,7 @@ export default {
name: 'DeviceSettingsDanger',
props: ['device'],
emits: ['device-updated'],
mixins: [permissionsMixin],
components: {
ConfirmDeviceDeleteDialog,
FormHeading
Expand All @@ -46,7 +47,7 @@ export default {
},
methods: {
checkAccess: async function () {
if (this.teamMembership && this.teamMembership.role < Roles.Owner) {
if (!this.hasPermission('device:edit')) {
useRouter().push({ replace: true, path: 'general' })
}
},
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/pages/device/Settings/Environment.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
<template>
<form class="space-y-6">
<TemplateSettingsEnvironment v-model="editable" :editTemplate="false" />
<div class="space-x-4 whitespace-nowrap">
<TemplateSettingsEnvironment :readOnly="!hasPermission('device:edit-env')" v-model="editable" :editTemplate="false" />
<div v-if="hasPermission('device:edit-env')" class="space-x-4 whitespace-nowrap">
<ff-button size="small" :disabled="!unsavedChanges" @click="saveSettings()">Save Settings</ff-button>
</div>
</form>
</template>

<script>
import { mapState } from 'vuex'
import deviceApi from '@/api/devices'
import permissionsMixin from '@/mixins/Permissions'
import TemplateSettingsEnvironment from '../../admin/Template/sections/Environment'
export default {
name: 'DeviceSettingsEnvironment',
props: ['device'],
emits: ['device-updated'],
mixins: [permissionsMixin],
watch: {
device: 'getSettings',
'editable.settings.env': {
Expand Down Expand Up @@ -78,6 +82,9 @@ export default {
mounted () {
this.getSettings()
},
computed: {
...mapState('account', ['teamMembership'])
},
methods: {
getSettings: async function () {
if (this.device) {
Expand Down
10 changes: 4 additions & 6 deletions frontend/src/pages/device/Settings/General.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Name
</FormRow>

<div v-if="isOwner">
<div v-if="hasPermission('device:edit')">
<ff-button v-if="!editing.deviceName" kind="primary" @click="editDevice">Edit Device</ff-button>
<ff-button v-else kind="primary" @click="updateDevice">Save Changes</ff-button>
</div>
Expand All @@ -20,12 +20,13 @@ import deviceApi from '@/api/devices'
import FormRow from '@/components/FormRow'
import { mapState } from 'vuex'
import { Roles } from '@core/lib/roles'
import permissionsMixin from '@/mixins/Permissions'
export default {
name: 'DeviceSettings',
props: ['device'],
emits: ['device-updated'],
mixins: [permissionsMixin],
data () {
return {
editing: {
Expand All @@ -44,10 +45,7 @@ export default {
device: 'fetchData'
},
computed: {
...mapState('account', ['teamMembership']),
isOwner: function () {
return this.teamMembership.role >= Roles.Owner
}
...mapState('account', ['teamMembership'])
},
mounted () {
this.fetchData()
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/pages/device/Settings/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import SectionSideMenu from '@/components/SectionSideMenu'
import { mapState } from 'vuex'
import { useRouter } from 'vue-router'
import { Roles } from '@core/lib/roles'
import permissionsMixin from '@/mixins/Permissions'
export default {
name: 'DeviceSettins',
props: ['device'],
emits: ['device-updated'],
mixins: [permissionsMixin],
data: function () {
return {
sideNavigation: []
Expand All @@ -39,7 +40,7 @@ export default {
{ name: 'General', path: './general' },
{ name: 'Environment', path: './environment' }
]
if (this.teamMembership && this.teamMembership.role >= Roles.Owner) {
if (this.hasPermission('device:edit')) {
this.sideNavigation.push({ name: 'Danger', path: './danger' })
}
}
Expand Down

0 comments on commit d49255b

Please sign in to comment.