Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[full-ci] Add support for a public link permission #8541

Merged
merged 2 commits into from
Mar 6, 2023
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
2 changes: 1 addition & 1 deletion .drone.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# The version of OCIS to use in pipelines that test against OCIS
OCIS_COMMITID=5ad8e283b669a7270e6925bf653a636610e022ed
OCIS_COMMITID=22c99993f50a6e011cf0ea2743b54faac265c5cc
OCIS_BRANCH=master
2 changes: 1 addition & 1 deletion .drone.star
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ OC_CI_CORE_NODEJS = "owncloudci/core:nodejs14"
OC_CI_DRONE_ANSIBLE = "owncloudci/drone-ansible:latest"
OC_CI_DRONE_CANCEL_PREVIOUS_BUILDS = "owncloudci/drone-cancel-previous-builds"
OC_CI_DRONE_SKIP_PIPELINE = "owncloudci/drone-skip-pipeline"
OC_CI_GOLANG = "owncloudci/golang:1.18"
OC_CI_GOLANG = "owncloudci/golang:1.19"
OC_CI_HUGO = "owncloudci/hugo:0.89.4"
OC_CI_NODEJS = "owncloudci/nodejs:16"
OC_CI_PHP = "owncloudci/php:7.4"
Expand Down
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-public-link-permission
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Public link permission `PublicLink.Write.all`

Support for the public link permission `PublicLink.Write.all` has been added. Users without this permission won't be able to create public links (except for oC10 instances where this permission is being set implicitly).

https://github.com/owncloud/web/pull/8541
https://github.com/owncloud/web/issues/8540
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
appearance="raw"
class="oc-mr-xs quick-action-button oc-p-xs"
:class="`files-quick-action-${action.id}`"
@click="action.handler({ ...$language, item, client: $client, store: $store })"
@click="
action.handler({ ...$language, item, client: $client, store: $store, ability: $ability })
"
>
<oc-icon :name="action.icon" fill-type="line" />
</oc-button>
Expand Down
29 changes: 17 additions & 12 deletions packages/web-app-files/src/components/SideBar/Shares/FileLinks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
<oc-contextual-helper v-if="helpersEnabled" class="oc-pl-xs" v-bind="viaLinkHelp" />
</div>
<p
v-if="!canCreatePublicLinks"
v-if="!canCreateLinks"
data-testid="files-links-no-reshare-permissions-message"
class="oc-mt-m"
v-text="noResharePermsMessage"
/>
<div class="oc-mt-m">
<name-and-copy v-if="quicklink" :link="quicklink" />
<create-quick-link
v-else-if="canCreatePublicLinks"
v-else-if="canCreateLinks"
:expiration-date="expirationDate"
@create-public-link="checkLinkToCreate"
/>
Expand All @@ -31,7 +31,7 @@
/>
<hr class="oc-my-m" />
<oc-button
v-if="canCreatePublicLinks"
v-if="canCreateLinks"
id="files-file-link-add"
variation="primary"
appearance="raw"
Expand Down Expand Up @@ -117,7 +117,8 @@ import {
useCapabilityFilesSharingResharing,
useCapabilityFilesSharingPublicCanEdit,
useCapabilityFilesSharingPublicCanContribute,
useCapabilityFilesSharingPublicAlias
useCapabilityFilesSharingPublicAlias,
useAbility
} from 'web-pkg/src/composables'
import { shareViaLinkHelp, shareViaIndirectLinkHelp } from '../../../helpers/contextualHelpers'
import { LinkShareRoles, SharePermissions } from 'web-client/src/helpers/share'
Expand All @@ -140,6 +141,7 @@ export default defineComponent({
},
setup() {
const store = useStore()
const { can } = useAbility()

const linkListCollapsed = !store.getters.configuration.options.sidebar.shares.showAllOnLoad
const indirectLinkListCollapsed = ref(linkListCollapsed)
Expand All @@ -160,6 +162,7 @@ export default defineComponent({
return { ...share, key: 'indirect-link-' + share.id }
})
)
const canCreatePublicLinks = computed(() => can('create-all', 'PublicLink'))

return {
...useGraphClient(),
Expand All @@ -176,7 +179,8 @@ export default defineComponent({
linkListCollapsed,
outgoingLinks,
directLinks,
indirectLinks
indirectLinks,
canCreatePublicLinks
}
},
computed: {
Expand Down Expand Up @@ -255,7 +259,7 @@ export default defineComponent({
return shareViaIndirectLinkHelp
},

canCreatePublicLinks() {
canCreateLinks() {
if (this.resource.isReceivedShare() && !this.hasResharing) {
return false
}
Expand All @@ -269,7 +273,7 @@ export default defineComponent({
},

canEdit() {
return this.canCreatePublicLinks
return this.canCreateLinks
},

noResharePermsMessage() {
Expand Down Expand Up @@ -363,7 +367,7 @@ export default defineComponent({
this.checkLinkToCreate({
link: {
name: this.$gettext('Link'),
permissions: 1,
permissions: this.canCreatePublicLinks ? 1 : 0,
expiration: this.expirationDate.default,
password: false
}
Expand Down Expand Up @@ -452,7 +456,6 @@ export default defineComponent({
await this.addLink({
path,
client: this.$client,
$gettext: this.$gettext,
storageId: this.resource.fileId || this.resource.id,
params
}).catch((e) => {
Expand Down Expand Up @@ -541,14 +544,15 @@ export default defineComponent({
},

getAvailableRoleOptions(link) {
if (this.incomingParentShare && this.canCreatePublicLinks) {
if (this.incomingParentShare && this.canCreateLinks) {
return LinkShareRoles.filterByBitmask(
this.incomingParentShare.permissions,
this.resource.isFolder,
this.hasPublicLinkEditing,
this.hasPublicLinkContribute,
this.hasPublicLinkAliasSupport,
!!link.password
!!link.password,
this.canCreatePublicLinks
)
}

Expand All @@ -557,7 +561,8 @@ export default defineComponent({
this.hasPublicLinkEditing,
this.hasPublicLinkContribute,
this.hasPublicLinkAliasSupport,
!!link.password
!!link.password,
this.canCreatePublicLinks
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { computed, defineComponent } from 'vue'
import { useAbility } from 'web-pkg'

export default defineComponent({
name: 'CreateQuickLink',
Expand All @@ -30,6 +31,11 @@ export default defineComponent({
}
},
emits: ['createPublicLink'],
setup() {
const { can } = useAbility()
const canCreatePublicLinks = computed(() => can('create-all', 'PublicLink'))
return { canCreatePublicLinks }
},
computed: {
heading() {
return this.$gettext('Quick link')
Expand All @@ -49,7 +55,7 @@ export default defineComponent({
this.$emit('createPublicLink', {
link: {
name: this.$gettext('Quicklink'),
permissions: 1,
permissions: this.canCreatePublicLinks ? 1 : 0,
expiration: this.expirationDate.enforced ? this.expirationDate.default : null,
quicklink: true,
password: false
Expand Down
6 changes: 4 additions & 2 deletions packages/web-app-files/src/helpers/share/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ import { Share } from 'web-client/src/helpers/share'
import { Store } from 'vuex'
import { clientService } from 'web-pkg/src/services'
import { useClipboard } from '@vueuse/core'
import { Ability } from 'web-pkg'

interface CreateQuicklink {
store: Store<any>
storageId?: any
resource: any
password?: string
$gettext: (string) => string
ability: Ability
}

export const createQuicklink = async (args: CreateQuicklink): Promise<Share> => {
const { resource, store, password, $gettext } = args
const { resource, store, password, $gettext, ability } = args
const params: { [key: string]: unknown } = {
name: $gettext('Quicklink'),
permissions: 1,
permissions: ability.can('create-all', 'PublicLink') ? '1' : '0',
quicklink: true
}

Expand Down
3 changes: 2 additions & 1 deletion packages/web-app-files/src/mixins/actions/createQuicklink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export default {
resource,
storageId: this.space?.id || resource?.fileId || resource?.id,
store,
$gettext: this.$gettext
$gettext: this.$gettext,
ability: this.$ability
})
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/web-app-files/src/quickActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ export default {

if (passwordEnforced) {
return showQuickLinkPasswordModal(ctx, async (password) => {
await createQuicklink({ ...ctx, resource: ctx.item, password })
await createQuicklink({ ...ctx, resource: ctx.item, password, ability: ctx.ability })
})
}

await createQuicklink({ ...ctx, resource: ctx.item })
await createQuicklink({ ...ctx, resource: ctx.item, ability: ctx.ability })
},
displayed: canShare
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from 'web-test-helpers'
import { mockDeep } from 'jest-mock-extended'
import { Resource } from 'web-client'
import { SharePermissions } from 'web-client/src/helpers/share'

const defaultLinksList = [
{
Expand Down Expand Up @@ -72,7 +73,7 @@ describe('FileLinks', () => {
expect(wrapper.find('oc-list-stub').exists()).toBeFalsy()
})
})
describe('when canCreatePublicLinks is set to true', () => {
describe('when canCreateLinks is set to true', () => {
it('should show a button to add a link', () => {
const { wrapper } = getWrapper()
expect(wrapper.find(selectors.linkAddButton).exists()).toBeTruthy()
Expand All @@ -88,7 +89,7 @@ describe('FileLinks', () => {
})
})
})
describe('when canCreatePublicLinks is set to false', () => {
describe('when canCreateLinks is set to false', () => {
it('should show the "no reshare permissions" message', () => {
const resource = mockDeep<Resource>({
path: '/lorem.txt',
Expand All @@ -101,11 +102,41 @@ describe('FileLinks', () => {
expect(wrapper.find(selectors.noResharePermissions).exists()).toBeTruthy()
})
})
describe('user does not have the permission to create public links', () => {
it('only shows internal role option', () => {
const resource = mockDeep<Resource>({
path: '/lorem.txt',
type: 'file',
isFolder: false,
isReceivedShare: jest.fn()
})
const { wrapper } = getWrapper({ resource, abilities: [] })
const availableRoleOptions = wrapper
.findComponent<any>(linkListItemDetailsAndEdit)
.props('availableRoleOptions')
expect(availableRoleOptions.length).toBe(1)
expect(availableRoleOptions[0].permissions()).toEqual([SharePermissions.internal])
})
it('creates new links with permission 0', async () => {
const { wrapper, storeOptions } = getWrapper({ abilities: [] })
await wrapper.find(selectors.linkAddButton).trigger('click')
expect(storeOptions.modules.Files.actions.addLink).toHaveBeenCalledTimes(1)
expect(storeOptions.modules.Files.actions.addLink).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
params: expect.objectContaining({
permissions: '0'
})
})
)
})
})
})

function getWrapper({
resource = mockDeep<Resource>({ isFolder: false, canShare: () => true }),
links = defaultLinksList
links = defaultLinksList,
abilities = [{ action: 'create-all', subject: 'PublicLink' }]
} = {}) {
const storeOptions = {
...defaultStoreMockOptions,
Expand All @@ -120,6 +151,7 @@ function getWrapper({
public: {
defaultPublicLinkShareName: 'public link name default',
expire_date: new Date(),
alias: true,
password: {
enforced_for: {
read_only: false,
Expand All @@ -136,9 +168,10 @@ function getWrapper({
defaultStoreMockOptions.modules.Files.getters.outgoingLinks.mockReturnValue(links)
const store = createStore(storeOptions)
return {
storeOptions,
wrapper: shallowMount(FileLinks, {
global: {
plugins: [...defaultPlugins(), store],
plugins: [...defaultPlugins({ abilities }), store],
renderStubDefaultSlot: true,
stubs: { OcButton: false },
provide: {
Expand Down
34 changes: 21 additions & 13 deletions packages/web-client/src/helpers/share/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,16 +361,17 @@ export abstract class LinkShareRoles {
canEditFile = false,
canContribute = false,
hasAliasLinks = false,
hasPassword = false
hasPassword = false,
canCreatePublicLinks = true
): ShareRole[] {
return [
...(hasAliasLinks && !hasPassword ? [linkRoleInternalFile, linkRoleInternalFolder] : []),
linkRoleViewerFile,
linkRoleViewerFolder,
...(canContribute ? [linkRoleContributorFolder] : []),
linkRoleEditorFolder,
linkRoleUploaderFolder,
...(canEditFile ? [linkRoleEditorFile] : [])
...(canCreatePublicLinks ? [linkRoleViewerFile] : []),
...(canCreatePublicLinks ? [linkRoleViewerFolder] : []),
...(canCreatePublicLinks && canContribute ? [linkRoleContributorFolder] : []),
...(canCreatePublicLinks ? [linkRoleEditorFolder] : []),
...(canCreatePublicLinks ? [linkRoleUploaderFolder] : []),
...(canCreatePublicLinks && canEditFile ? [linkRoleEditorFile] : [])
].filter((r) => r.folder === isFolder)
}

Expand All @@ -386,20 +387,27 @@ export abstract class LinkShareRoles {
* @param canContribute
* @param hasAliasLinks
* @param hasPassword
* @param canCreatePublicLinks
*/
static filterByBitmask(
bitmask: number,
isFolder: boolean,
canEditFile = false,
canContribute = false,
hasAliasLinks = false,
hasPassword = false
hasPassword = false,
canCreatePublicLinks = true
): ShareRole[] {
return this.list(isFolder, canEditFile, canContribute, hasAliasLinks, hasPassword).filter(
(r) => {
return bitmask === (bitmask | r.bitmask(false))
}
)
return this.list(
isFolder,
canEditFile,
canContribute,
hasAliasLinks,
hasPassword,
canCreatePublicLinks
).filter((r) => {
return bitmask === (bitmask | r.bitmask(false))
})
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/web-pkg/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type AbilitySubjects =
| 'Group'
| 'Language'
| 'Logo'
| 'PublicLink'
| 'Role'
| 'Setting'
| 'Space'
Expand Down
Loading