Skip to content

Commit

Permalink
Use CASL for handling permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
JammingBen committed Feb 14, 2023
1 parent 480ee64 commit 57a0f52
Show file tree
Hide file tree
Showing 20 changed files with 166 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ import {
} from 'web-test-helpers'
import Spaces from '../../../src/views/Spaces.vue'

window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn()
}))

const selectors = {
loadingSpinnerStub: 'app-loading-spinner-stub',
spacesListStub: 'spaces-list-stub',
Expand Down
11 changes: 6 additions & 5 deletions packages/web-app-files/src/views/spaces/Projects.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import AppLoadingSpinner from 'web-pkg/src/components/AppLoadingSpinner.vue'
import AppBar from '../../components/AppBar/AppBar.vue'
import CreateSpace from '../../components/AppBar/CreateSpace.vue'
import { useAccessToken, useStore, useGraphClient } from 'web-pkg/src/composables'
import { useAbility, useAccessToken, useStore, useGraphClient } from 'web-pkg/src/composables'
import { loadPreview } from 'web-pkg/src/helpers/preview'
import { ImageDimension } from 'web-pkg/src/constants'
import SpaceContextActions from '../../components/Spaces/SpaceContextActions.vue'
Expand Down Expand Up @@ -95,6 +95,7 @@ export default defineComponent({
setup() {
const store = useStore()
const { selectedResourcesIds } = useSelectedResources({ store })
const { can } = useAbility()
const spaces = computed(
() =>
Expand All @@ -118,6 +119,8 @@ export default defineComponent({
return loadResourcesTask.isRunning || !loadResourcesTask.last
})
const hasCreatePermission = computed(() => can('create', 'Space'))
return {
...useSideBar(),
...useScrollTo(),
Expand All @@ -126,7 +129,8 @@ export default defineComponent({
loadResourcesTask,
areResourcesLoading,
accessToken,
selectedResourcesIds
selectedResourcesIds,
hasCreatePermission
}
},
data: function () {
Expand All @@ -147,9 +151,6 @@ export default defineComponent({
},
showSpaceMemberLabel() {
return this.$gettext('Show members')
},
hasCreatePermission() {
return this.$permissionManager.hasSpaceManagement()
}
},
watch: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,6 @@ const breadCrumbItemWithContextActionAllowed = {
allowContextActions: true
}

window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn()
}))

describe('AppBar component', () => {
describe('renders', () => {
it('by default no breadcrumbs, no bulkactions, no sharesnavigation but viewoptions and sidebartoggle', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@ import {
defaultComponentMocks
} from 'web-test-helpers'

window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn()
}))

afterEach(() => jest.clearAllMocks())

describe('SpaceHeader', () => {
it('should add the "squashed"-class when the sidebar is opened', () => {
const wrapper = getWrapper({ space: buildSpace({ id: 1 }), sideBarOpen: true })
Expand Down
18 changes: 15 additions & 3 deletions packages/web-app-files/tests/unit/views/spaces/Projects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,16 @@ describe('Projects view', () => {
expect(wrapper.findAll('.oc-tiles-item').length).toEqual(spaces.length)
})
})
it('should display the "Create Space"-button when permission given', () => {
const { wrapper } = getMountedWrapper({
abilities: [{ action: 'create', subject: 'Space' }],
stubAppBar: false
})
expect(wrapper.find('create-space-stub').exists()).toBeTruthy()
})
})

function getMountedWrapper({ mocks = {}, spaces = [] } = {}) {
function getMountedWrapper({ mocks = {}, spaces = [], abilities = [], stubAppBar = true } = {}) {
const defaultMocks = {
...defaultComponentMocks({
currentRoute: mock<RouteLocation>({ name: 'files-spaces-projects' })
Expand All @@ -83,9 +90,14 @@ function getMountedWrapper({ mocks = {}, spaces = [] } = {}) {
storeOptions,
wrapper: mount(Projects, {
global: {
plugins: [...defaultPlugins(), store],
plugins: [...defaultPlugins({ abilities }), store],
mocks: defaultMocks,
stubs: { ...defaultStubs, 'space-context-actions': true }
stubs: {
...defaultStubs,
'space-context-actions': true,
'app-bar': stubAppBar,
CreateSpace: true
}
}
})
}
Expand Down
8 changes: 0 additions & 8 deletions packages/web-app-search/tests/unit/portals/SearchBar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,6 @@ const selectors = {
jest.mock('lodash-es/debounce', () => (fn) => fn)

beforeEach(() => {
jest.resetAllMocks()

window.ResizeObserver = jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn()
}))

providerFiles.previewSearch.search.mockImplementation(() => {
return {
values: [
Expand Down
2 changes: 2 additions & 0 deletions packages/web-pkg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"directory": "packages/web-pkg"
},
"peerDependencies": {
"@casl/ability": "^6.3.3",
"@casl/vue": "^2.2.1",
"axios": "^0.27.2",
"filesize": "^9.0.11",
"fuse.js": "^6.5.3",
Expand Down
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/ability/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useAbility'
4 changes: 4 additions & 0 deletions packages/web-pkg/src/composables/ability/useAbility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { useAbility as _useAbility } from '@casl/vue'
import { Ability } from './../../utils'

export const useAbility = () => _useAbility<Ability>()
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './ability'
export * from './appDefaults'
export * from './authContext'
export * from './capability'
Expand Down
6 changes: 6 additions & 0 deletions packages/web-pkg/src/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Ref } from 'vue'
import { MongoAbility } from '@casl/ability'

export type ReadOnlyRef<T> = Readonly<Ref<T>>
export type MaybeRef<T> = T | Ref<T>
export type MaybeReadonlyRef<T> = MaybeRef<T> | ReadOnlyRef<T>

export type Actions = 'create' | 'read' | 'update' | 'delete' | 'manage'
export type Subjects = 'Space'

export type Ability = MongoAbility<[Actions, Subjects]>
2 changes: 2 additions & 0 deletions packages/web-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"description": "ownCloud web runtime",
"license": "AGPL-3.0",
"dependencies": {
"@casl/ability": "^6.3.3",
"@casl/vue": "^2.2.1",
"@fortawesome/fontawesome-free": "6.2.1",
"@ownclouders/design-system": "workspace:*",
"@popperjs/core": "^2.11.5",
Expand Down
3 changes: 2 additions & 1 deletion packages/web-runtime/src/container/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ export const announceAuthService = ({
store: Store<any>
router: Router
}): void => {
authService.initialize(configurationManager, clientService, store, router)
const $ability = app.config.globalProperties.$ability
authService.initialize(configurationManager, clientService, store, router, $ability)
app.config.globalProperties.$authService = authService
}

Expand Down
4 changes: 4 additions & 0 deletions packages/web-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { DesignSystem as designSystem, pages, translations, supportedLanguages }
import { router } from './router'
import { configurationManager } from 'web-pkg/src/configuration'
import { createHead } from '@vueuse/head'
import { abilitiesPlugin } from '@casl/vue'
import { createMongoAbility } from '@casl/ability'

import {
announceConfiguration,
initializeApplications,
Expand Down Expand Up @@ -46,6 +49,7 @@ export const bootstrapApp = async (configurationPath: string): Promise<void> =>

const store = await announceStore({ runtimeConfiguration })
announcePermissionManager({ app, store })
app.use(abilitiesPlugin, createMongoAbility([]), { useGlobalProperties: true })

const applicationsPromise = await initializeApplications({
runtimeConfiguration,
Expand Down
9 changes: 7 additions & 2 deletions packages/web-runtime/src/services/auth/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ConfigurationManager } from 'web-pkg/src/configuration'
import { RouteLocation, Router } from 'vue-router'
import { extractPublicLinkToken, isPublicLinkContext, isUserContext } from '../../router'
import { unref } from 'vue'
import { Ability } from 'web-pkg/src/utils'

export class AuthService {
private clientService: ClientService
Expand All @@ -14,20 +15,23 @@ export class AuthService {
private router: Router
private userManager: UserManager
private publicLinkManager: PublicLinkManager
private $ability: Ability

public hasAuthErrorOccured: boolean

public initialize(
configurationManager: ConfigurationManager,
clientService: ClientService,
store: Store<any>,
router: Router
router: Router,
$ability: Ability
): void {
this.configurationManager = configurationManager
this.clientService = clientService
this.store = store
this.router = router
this.hasAuthErrorOccured = false
this.$ability = $ability
}

/**
Expand Down Expand Up @@ -61,7 +65,8 @@ export class AuthService {
this.userManager = new UserManager({
clientService: this.clientService,
configurationManager: this.configurationManager,
store: this.store
store: this.store,
$ability: this.$ability
})

this.userManager.events.addAccessTokenExpired((...args): void => {
Expand Down
17 changes: 17 additions & 0 deletions packages/web-runtime/src/services/auth/userManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Store } from 'vuex'
import isEmpty from 'lodash-es/isEmpty'
import axios from 'axios'
import { v4 as uuidV4 } from 'uuid'
import { Ability, Actions, Subjects } from 'web-pkg/src/utils'
import { SubjectRawRule } from '@casl/ability'

const postLoginRedirectUrlKey = 'oc.postLoginRedirectUrl'
type UnloadReason = 'authError' | 'logout'
Expand All @@ -19,6 +21,7 @@ export interface UserManagerOptions {
clientService: ClientService
configurationManager: ConfigurationManager
store: Store<any>
$ability: Ability
}

export class UserManager extends OidcUserManager {
Expand All @@ -28,6 +31,7 @@ export class UserManager extends OidcUserManager {
private store: Store<any>
private updateAccessTokenPromise: Promise<void> | null
private _unloadReason: UnloadReason
private $ability: Ability

constructor(options: UserManagerOptions) {
const storePrefix = 'oc_oAuth.'
Expand Down Expand Up @@ -84,6 +88,7 @@ export class UserManager extends OidcUserManager {
this.clientService = options.clientService
this.configurationManager = options.configurationManager
this.store = options.store
this.$ability = options.$ability
}

/**
Expand Down Expand Up @@ -132,6 +137,7 @@ export class UserManager extends OidcUserManager {

if (!userKnown) {
await this.fetchUserInfo(accessToken)
this.updateUserAbilities(this.store.getters.user)
this.store.commit('runtime/auth/SET_USER_CONTEXT_READY', true)
}
})()
Expand Down Expand Up @@ -266,4 +272,15 @@ export class UserManager extends OidcUserManager {

this.store.commit('SET_CAPABILITIES', capabilities)
}

private updateUserAbilities(user) {
const rules: SubjectRawRule<Actions, Subjects, any>[] = []

// TODO: expand capabilities
if (!!user.role?.settings.find((s) => s.name === 'create-space')) {
rules.push({ action: 'create', subject: 'Space' })
}

this.$ability.update(rules)
}
}
2 changes: 2 additions & 0 deletions packages/web-test-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"private": true,
"main": "src/index.ts",
"peerDependencies": {
"@casl/ability": "^6.3.3",
"@casl/vue": "^2.2.1",
"axios": "0.27.2",
"vue3-gettext": "^2.3.3",
"vue-router": "4.1.6",
Expand Down
13 changes: 12 additions & 1 deletion packages/web-test-helpers/src/defaultPlugins.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import DesignSystem from '@ownclouders/design-system'
import { createGettext } from 'vue3-gettext'
import { h } from 'vue'
import { abilitiesPlugin } from '@casl/vue'
import { createMongoAbility } from '@casl/ability'

export interface DefaultPluginsOptions {
designSystem?: boolean
gettext?: boolean
abilities?: any
}

export const defaultPlugins = ({
designSystem = true,
gettext = true
gettext = true,
abilities = []
}: DefaultPluginsOptions = {}) => {
const plugins = []

Expand All @@ -29,6 +34,12 @@ export const defaultPlugins = ({
})
}

plugins.push({
install(app) {
app.use(abilitiesPlugin, createMongoAbility(abilities))
}
})

plugins.push({
install(app) {
app.component('RouterLink', {
Expand Down
Loading

0 comments on commit 57a0f52

Please sign in to comment.