Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions frontend/src/composables/Permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ export const hasPermission = (scope, teamMembership, context) => {
return true
}

/**
* Checks if a user has the required permission either at the team level or in at least
* one application where they have an application-level role.
*
* Used for team-wide actions that end up in an application of the user's choosing,
* e.g. creating an instance from the team pages.
*
* @param {string} scope - The specific scope for which the permission check is being made.
* @param {Object|null} teamMembership - The user's team membership information.
* @returns {boolean} Returns true if the user has the permission in the team or in any of their applications.
*/
export const hasPermissionInAnyApplication = (scope, teamMembership) => {
if (hasPermission(scope, teamMembership)) {
return true
}
const applications = Object.keys(teamMembership?.permissions?.applications || {})
return applications.some(applicationId => hasPermission(scope, teamMembership, { applicationId }))
}

/**
* Check if the user has the minimum required role.
* @param {Role} role - The role to check against.
Expand Down Expand Up @@ -110,6 +129,14 @@ export default function usePermissions () {
*/
const _hasPermission = (scope, context) => hasPermission(scope, teamMembership.value, context)

/**
* Checks if a user has the required permission in the team or in at least one of their applications.
*
* @param {string} scope - The specific scope for which the permission check is being made.
* @returns {boolean} Returns true if the user has the permission in the team or in any of their applications.
*/
const _hasPermissionInAnyApplication = (scope) => hasPermissionInAnyApplication(scope, teamMembership.value)

/**
* Check if the user has the minimum required role.
* @param {Role} role - The role to check against.
Expand All @@ -133,6 +160,7 @@ export default function usePermissions () {
return {
isVisitingAdmin: _isVisitingAdmin,
hasPermission: _hasPermission,
hasPermissionInAnyApplication: _hasPermissionInAnyApplication,
hasAMinimumTeamRoleOf: _hasAMinimumTeamRoleOf,
hasALowerOrEqualTeamRoleThan: _hasALowerOrEqualTeamRoleThan
}
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/pages/team/Home/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@

<template #actions>
<ff-button
v-ff-tooltip:left="!hasPermission('project:create') && 'Your role does not allow creating new instances. Contact a team admin to change your role.'"
v-ff-tooltip:left="!hasPermissionInAnyApplication('project:create') && 'Your role does not allow creating new instances. Contact a team admin to change your role.'"
data-action="create-project"
kind="secondary"
:to="{name: 'team-instance-create'}"
:disabled="!hasPermission('project:create')"
:disabled="!hasPermissionInAnyApplication('project:create')"
>
<template #icon-left>
<PlusIcon class="ff-icon" />
Expand Down Expand Up @@ -189,10 +189,11 @@ export default {
setup () {
const { groupBySimplifiedStates } = useInstanceStates()

const { hasPermission } = usePermissions()
const { hasPermission, hasPermissionInAnyApplication } = usePermissions()
return {
groupBySimplifiedStates,
hasPermission
hasPermission,
hasPermissionInAnyApplication
}
},
data () {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/pages/team/Instances.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@
</template>
</ff-popover>
<ff-button
v-ff-tooltip:left="!hasPermission('project:create') && 'Your role does not allow creating new instances. Contact a team admin to change your role.'"
v-ff-tooltip:left="!hasPermissionInAnyApplication('project:create') && 'Your role does not allow creating new instances. Contact a team admin to change your role.'"
data-action="create-project"
kind="primary"
:to="{name: 'team-instance-create'}"
:disabled="!hasPermission('project:create')"
:disabled="!hasPermissionInAnyApplication('project:create')"
>
<template #icon-left>
<PlusSmallIcon />
Expand Down Expand Up @@ -148,10 +148,10 @@
</template>
<template #actions>
<ff-button
v-ff-tooltip:bottom="!hasPermission('project:create') && 'Your role does not allow creating new instances. Contact a team admin to change your role.'"
v-ff-tooltip:bottom="!hasPermissionInAnyApplication('project:create') && 'Your role does not allow creating new instances. Contact a team admin to change your role.'"
kind="primary"
:to="{name: 'team-instance-create'}"
:disabled="!hasPermission('project:create')"
:disabled="!hasPermissionInAnyApplication('project:create')"
>
<template #icon-left>
<PlusSmallIcon />
Expand Down Expand Up @@ -225,9 +225,9 @@ export default {
setup () {
const { statesMap } = useInstanceStates()
const { navigateTo } = useNavigationHelper()
const { hasPermission } = usePermissions()
const { hasPermission, hasPermissionInAnyApplication } = usePermissions()

return { hasPermission, navigateTo, statesMap }
return { hasPermission, hasPermissionInAnyApplication, navigateTo, statesMap }
},
data () {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -588,10 +588,11 @@ describe('FlowFuse - RBAC Member Contextual permissions', () => {
}
})
})
it('should not be able to create new instances', () => {
it('should be able to create new instances because they own an application', () => {
cy.get('[data-nav="team-instances"]').click()
cy.get('[data-action="create-project"]').should('exist')
cy.get('[data-action="create-project"]').should('be.disabled')
// the user has an owner role in application-5
cy.get('[data-action="create-project"]').should('not.be.disabled')

// todo: users without permissions to create an instance in any application should be redirected when accessing
// the instance creation form
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,10 +565,11 @@ describe('FlowFuse - RBAC Viewer Contextual permissions', () => {
}
})
})
it('should not be able to create new instances', () => {
it('should be able to create new instances because they own an application', () => {
cy.get('[data-nav="team-instances"]').click()
cy.get('[data-action="create-project"]').should('exist')
cy.get('[data-action="create-project"]').should('be.disabled')
// the user has an owner role in application-5
cy.get('[data-action="create-project"]').should('not.be.disabled')

// todo: users without permissions to create an instance in any application should be redirected when accessing
// the instance creation form
Expand Down
34 changes: 34 additions & 0 deletions test/unit/frontend/composables/Permissions.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from 'vitest'

import { Roles } from '../../../../forge/lib/roles.js'
import { hasPermissionInAnyApplication } from '../../../../frontend/src/composables/Permissions.js'

describe('hasPermissionInAnyApplication', () => {
test('allows a team Owner', () => {
expect(hasPermissionInAnyApplication('project:create', { role: Roles.Owner })).toBe(true)
})

test('denies a team Member with no application roles', () => {
expect(hasPermissionInAnyApplication('project:create', { role: Roles.Member })).toBe(false)
})

test('allows a team Member who is Owner of an application', () => {
const teamMembership = {
role: Roles.Member,
permissions: { applications: { app1: Roles.Viewer, app2: Roles.Owner } }
}
expect(hasPermissionInAnyApplication('project:create', teamMembership)).toBe(true)
})

test('denies a team Member whose application roles are all below Owner', () => {
const teamMembership = {
role: Roles.Member,
permissions: { applications: { app1: Roles.Viewer, app2: Roles.Member } }
}
expect(hasPermissionInAnyApplication('project:create', teamMembership)).toBe(false)
})

test('denies when there is no team membership', () => {
expect(hasPermissionInAnyApplication('project:create', null)).toBe(false)
})
})
Loading