diff --git a/.drone.env b/.drone.env index c0864c4c95b..45293a7b155 100644 --- a/.drone.env +++ b/.drone.env @@ -1,3 +1,3 @@ # The version of OCIS to use in pipelines that test against OCIS -OCIS_COMMITID=01d40c27833bf131db0c8251359e7a6cf3087bd0 +OCIS_COMMITID=d73351af89b609e06004148c506fce865f3407bd OCIS_BRANCH=master diff --git a/packages/web-app-files/src/services/folder.ts b/packages/web-app-files/src/services/folder.ts index 27f69b07fa2..51c23e2da91 100644 --- a/packages/web-app-files/src/services/folder.ts +++ b/packages/web-app-files/src/services/folder.ts @@ -12,7 +12,9 @@ import { useConfigStore, ConfigStore, ResourcesStore, - useResourcesStore + useResourcesStore, + SharesStore, + useSharesStore } from '@ownclouders/web-pkg' import { unref } from 'vue' import { ClientService } from '@ownclouders/web-pkg' @@ -38,6 +40,7 @@ export type TaskContext = { router: Router capabilityStore: CapabilityStore resourcesStore: ResourcesStore + sharesStore: SharesStore } export interface FolderLoader { @@ -68,6 +71,7 @@ export class FolderService { const clientService = useClientService() const configStore = useConfigStore() const resourcesStore = useResourcesStore() + const sharesStore = useSharesStore() const loader = this.loaders.find((l) => l.isEnabled() && l.isActive(unref(router))) if (!loader) { @@ -83,6 +87,7 @@ export class FolderService { spacesStore, capabilityStore, resourcesStore, + sharesStore, router } try { diff --git a/packages/web-app-files/src/services/folder/loaderSharedViaLink.ts b/packages/web-app-files/src/services/folder/loaderSharedViaLink.ts index 84f08f139ce..f5c3a53cd0f 100644 --- a/packages/web-app-files/src/services/folder/loaderSharedViaLink.ts +++ b/packages/web-app-files/src/services/folder/loaderSharedViaLink.ts @@ -2,8 +2,7 @@ import { FolderLoader, FolderLoaderTask, TaskContext } from '../folder' import { Router } from 'vue-router' import { useTask } from 'vue-concurrency' import { isLocationSharesActive } from '@ownclouders/web-pkg' -import { ShareTypes } from '@ownclouders/web-client/src/helpers/share' -import { aggregateResourceShares } from '@ownclouders/web-client/src/helpers/share' +import { buildOutgoingShareResource } from '@ownclouders/web-client/src/helpers/share/functionsNG' export class FolderLoaderSharedViaLink implements FolderLoader { public isEnabled(): boolean { @@ -15,9 +14,7 @@ export class FolderLoaderSharedViaLink implements FolderLoader { } public getTask(context: TaskContext): FolderLoaderTask { - const { userStore, spacesStore, clientService, configStore, capabilityStore, resourcesStore } = - context - const { owncloudSdk: client } = clientService + const { userStore, spacesStore, clientService, configStore, resourcesStore } = context // eslint-disable-next-line @typescript-eslint/no-unused-vars return useTask(function* (signal1, signal2) { @@ -28,28 +25,13 @@ export class FolderLoaderSharedViaLink implements FolderLoader { yield spacesStore.loadMountPoints({ graphClient: clientService.graphAuthenticated }) } - let resources = yield client.shares.getShares('', { - share_types: ShareTypes.link.value.toString(), - include_tags: false - }) - - resources = resources.map((r) => r.shareInfo) - if (resources.length) { - resources = aggregateResourceShares({ - shares: resources, - spaces: spacesStore.spaces, - incomingShares: false, - allowSharePermission: capabilityStore.sharingResharing, - hasShareJail: capabilityStore.spacesShareJail, - fullShareOwnerPaths: configStore.options.routing.fullShareOwnerPaths - }).map((resource) => { - // info: in oc10 we have no storageId in resources. All resources are mounted into the personal space. - if (!resource.storageId) { - resource.storageId = userStore.user.onPremisesSamAccountName - } - return resource - }) - } + const { + data: { value } + } = yield clientService.graphAuthenticated.drives.listSharedByMe() + + const resources = value + .filter((s) => s.permissions.some(({ link }) => !!link)) + .map((driveItem) => buildOutgoingShareResource({ driveItem, user: userStore.user })) resourcesStore.initResourceList({ currentFolder: null, resources }) }) diff --git a/packages/web-app-files/src/services/folder/loaderSharedWithMe.ts b/packages/web-app-files/src/services/folder/loaderSharedWithMe.ts index 8ee2231cf07..89cbdbddf6b 100644 --- a/packages/web-app-files/src/services/folder/loaderSharedWithMe.ts +++ b/packages/web-app-files/src/services/folder/loaderSharedWithMe.ts @@ -1,7 +1,7 @@ import { FolderLoader, FolderLoaderTask, TaskContext } from '../folder' import { Router } from 'vue-router' import { useTask } from 'vue-concurrency' -import { aggregateResourceShares } from '@ownclouders/web-client/src/helpers/share' +import { buildIncomingShareResource } from '@ownclouders/web-client/src/helpers/share/functionsNG' import { isLocationSharesActive } from '@ownclouders/web-pkg' export class FolderLoaderSharedWithMe implements FolderLoader { @@ -14,8 +14,7 @@ export class FolderLoaderSharedWithMe implements FolderLoader { } public getTask(context: TaskContext): FolderLoaderTask { - const { userStore, spacesStore, clientService, configStore, capabilityStore, resourcesStore } = - context + const { spacesStore, clientService, configStore, resourcesStore, sharesStore } = context // eslint-disable-next-line @typescript-eslint/no-unused-vars return useTask(function* (signal1, signal2) { @@ -26,31 +25,13 @@ export class FolderLoaderSharedWithMe implements FolderLoader { yield spacesStore.loadMountPoints({ graphClient: clientService.graphAuthenticated }) } - let resources = yield clientService.owncloudSdk.shares.getShares('', { - state: 'all', - include_tags: false, - shared_with_me: true, - show_hidden: true - }) - - resources = resources.map((r) => r.shareInfo) - - if (resources.length) { - resources = aggregateResourceShares({ - shares: resources, - spaces: spacesStore.spaces, - incomingShares: true, - allowSharePermission: capabilityStore.sharingResharing, - hasShareJail: capabilityStore.spacesShareJail, - fullShareOwnerPaths: configStore.options.routing.fullShareOwnerPaths - }).map((resource) => { - // info: in oc10 we have no storageId in resources. All resources are mounted into the personal space. - if (!resource.storageId) { - resource.storageId = userStore.user.onPremisesSamAccountName - } - return resource - }) - } + const { + data: { value } + } = yield clientService.graphAuthenticated.drives.listSharedWithMe() + + const resources = value.map((driveItem) => + buildIncomingShareResource({ driveItem, graphRoles: sharesStore.graphRoles }) + ) resourcesStore.initResourceList({ currentFolder: null, resources }) }) diff --git a/packages/web-app-files/src/services/folder/loaderSharedWithOthers.ts b/packages/web-app-files/src/services/folder/loaderSharedWithOthers.ts index 13493a9fc10..b07f65fec80 100644 --- a/packages/web-app-files/src/services/folder/loaderSharedWithOthers.ts +++ b/packages/web-app-files/src/services/folder/loaderSharedWithOthers.ts @@ -2,8 +2,7 @@ import { FolderLoader, FolderLoaderTask, TaskContext } from '../folder' import { Router } from 'vue-router' import { useTask } from 'vue-concurrency' import { isLocationSharesActive } from '@ownclouders/web-pkg' -import { aggregateResourceShares } from '@ownclouders/web-client/src/helpers/share' -import { peopleRoleDenyFolder, ShareTypes } from '@ownclouders/web-client/src/helpers/share' +import { buildOutgoingShareResource } from '@ownclouders/web-client/src/helpers/share/functionsNG' export class FolderLoaderSharedWithOthers implements FolderLoader { public isEnabled(): boolean { @@ -15,9 +14,7 @@ export class FolderLoaderSharedWithOthers implements FolderLoader { } public getTask(context: TaskContext): FolderLoaderTask { - const { userStore, spacesStore, clientService, configStore, capabilityStore, resourcesStore } = - context - const { owncloudSdk: client } = clientService + const { userStore, spacesStore, clientService, configStore, resourcesStore } = context // eslint-disable-next-line @typescript-eslint/no-unused-vars return useTask(function* (signal1, signal2) { @@ -28,37 +25,13 @@ export class FolderLoaderSharedWithOthers implements FolderLoader { yield spacesStore.loadMountPoints({ graphClient: clientService.graphAuthenticated }) } - const shareTypes = ShareTypes.authenticated - .filter( - (type) => ![ShareTypes.spaceUser.value, ShareTypes.spaceGroup.value].includes(type.value) - ) - .map((share) => share.value) - .join(',') + const { + data: { value } + } = yield clientService.graphAuthenticated.drives.listSharedByMe() - let resources = yield client.shares.getShares('', { - share_types: shareTypes, - reshares: true, - include_tags: false - }) - resources = resources - .filter((r) => parseInt(r.shareInfo.permissions) !== peopleRoleDenyFolder.bitmask(false)) - .map((r) => r.shareInfo) - if (resources.length) { - resources = aggregateResourceShares({ - shares: resources, - spaces: spacesStore.spaces, - incomingShares: false, - allowSharePermission: capabilityStore.sharingResharing, - hasShareJail: capabilityStore.spacesShareJail, - fullShareOwnerPaths: configStore.options.routing.fullShareOwnerPaths - }).map((resource) => { - // info: in oc10 we have no storageId in resources. All resources are mounted into the personal space. - if (!resource.storageId) { - resource.storageId = userStore.user.onPremisesSamAccountName - } - return resource - }) - } + const resources = value + .filter((s) => s.permissions.some(({ link }) => !link)) + .map((driveItem) => buildOutgoingShareResource({ driveItem, user: userStore.user })) resourcesStore.initResourceList({ currentFolder: null, resources }) }) diff --git a/packages/web-app-files/src/views/shares/SharedWithMe.vue b/packages/web-app-files/src/views/shares/SharedWithMe.vue index ae7ad49b98a..afe37d60954 100644 --- a/packages/web-app-files/src/views/shares/SharedWithMe.vue +++ b/packages/web-app-files/src/views/shares/SharedWithMe.vue @@ -181,8 +181,10 @@ export default defineComponent({ const selectedShareTypes = queryItemAsString(unref(selectedShareTypesQuery))?.split('+') if (selectedShareTypes?.length) { - result = result.filter(({ shareType }) => { - return selectedShareTypes.map((t) => ShareTypes[t].value).includes(shareType) + result = result.filter(({ shareTypes }) => { + return selectedShareTypes + .map((t) => ShareTypes[t].value) + .some((t) => shareTypes.includes(t)) }) } @@ -244,7 +246,7 @@ export default defineComponent({ } const shareTypes = computed(() => { - const uniqueShareTypes = uniq(unref(paginatedResources).map((i) => i.shareType)) + const uniqueShareTypes = uniq(unref(paginatedResources).flatMap((i) => i.shareTypes)) return ShareTypes.getByValues(uniqueShareTypes) }) diff --git a/packages/web-app-files/tests/unit/components/SideBar/Details/FileDetails.spec.ts b/packages/web-app-files/tests/unit/components/SideBar/Details/FileDetails.spec.ts index c279742b909..f9fa2800ebb 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Details/FileDetails.spec.ts +++ b/packages/web-app-files/tests/unit/components/SideBar/Details/FileDetails.spec.ts @@ -35,7 +35,7 @@ const getResourceMock = ({ shareTypes, locked, canEditTags: vi.fn(() => canEditTags), - ...(sharedBy && { shareType: 0 }) + ...(sharedBy && { sharedWith: [] }) }) const selectors = { diff --git a/packages/web-app-files/tests/unit/views/shares/SharedWithMe.spec.ts b/packages/web-app-files/tests/unit/views/shares/SharedWithMe.spec.ts index f0bd383ea86..b4a95078b42 100644 --- a/packages/web-app-files/tests/unit/views/shares/SharedWithMe.spec.ts +++ b/packages/web-app-files/tests/unit/views/shares/SharedWithMe.spec.ts @@ -84,8 +84,8 @@ describe('SharedWithMe view', () => { const shareType2 = ShareTypes.group const { wrapper } = getMountedWrapper({ files: [ - mock({ shareType: shareType1.value }), - mock({ shareType: shareType2.value }) + mock({ shareTypes: [shareType1.value] }), + mock({ shareTypes: [shareType2.value] }) ] }) const filterItems = wrapper.findComponent('.share-type-filter').props('items') @@ -101,11 +101,11 @@ describe('SharedWithMe view', () => { files: [ mock({ sharedBy: collaborator1, - shareType: ShareTypes.user.value + shareTypes: [ShareTypes.user.value] }), mock({ sharedBy: collaborator2, - shareType: ShareTypes.user.value + shareTypes: [ShareTypes.user.value] }) ] }) @@ -125,12 +125,12 @@ describe('SharedWithMe view', () => { mock({ name: 'share1', hidden: false, - shareType: ShareTypes.user.value + shareTypes: [ShareTypes.user.value] }), mock({ name: 'share2', hidden: false, - shareType: ShareTypes.user.value + shareTypes: [ShareTypes.user.value] }) ] }) diff --git a/packages/web-client/src/graph.ts b/packages/web-client/src/graph.ts index 27c8591bcc0..6b1c1a8caea 100644 --- a/packages/web-client/src/graph.ts +++ b/packages/web-client/src/graph.ts @@ -24,7 +24,11 @@ import { ApplicationsApiFactory, UserAppRoleAssignmentApiFactory, AppRoleAssignment, - ExportPersonalDataRequest + ExportPersonalDataRequest, + MeDriveApiFactory, + RoleManagementApiFactory, + UnifiedRoleDefinition, + CollectionOfDriveItems1 } from './generated' export interface Graph { @@ -39,6 +43,8 @@ export interface Graph { drives: { listMyDrives: (orderBy?: string, filter?: string) => Promise> listAllDrives: (orderBy?: string, filter?: string) => Promise> + listSharedWithMe: () => AxiosPromise + listSharedByMe: () => AxiosPromise getDrive: (id: string) => AxiosPromise createDrive: (drive: Drive, options: any) => AxiosPromise updateDrive: (id: string, drive: Drive, options: any) => AxiosPromise @@ -72,6 +78,9 @@ export interface Graph { addMember: (groupId: string, userId: string, server: string) => AxiosPromise deleteMember: (groupId: string, userId: string) => AxiosPromise } + roleManagement: { + listPermissionRoleDefinitions: () => AxiosPromise + } } export const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => { @@ -84,6 +93,7 @@ export const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => { const meDrivesApi = new MeDrivesApi(config, config.basePath, axiosClient) const allDrivesApi = new DrivesGetDrivesApi(config, config.basePath, axiosClient) const meUserApiFactory = MeUserApiFactory(config, config.basePath, axiosClient) + const meDriveApiFactory = MeDriveApiFactory(config, config.basePath, axiosClient) const meChangepasswordApiFactory = MeChangepasswordApiFactory( config, config.basePath, @@ -101,6 +111,7 @@ export const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => { const groupsApiFactory = GroupsApiFactory(config, config.basePath, axiosClient) const drivesApiFactory = DrivesApiFactory(config, config.basePath, axiosClient) const tagsApiFactory = TagsApiFactory(config, config.basePath, axiosClient) + const roleManagementApiFactory = RoleManagementApiFactory(config, config.basePath, axiosClient) return { applications: { @@ -117,6 +128,8 @@ export const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => { meDrivesApi.listMyDrives(orderBy, filter), listAllDrives: (orderBy?: string, filter?: string) => allDrivesApi.listAllDrives(orderBy, filter), + listSharedWithMe: () => meDriveApiFactory.listSharedWithMe(), + listSharedByMe: () => meDriveApiFactory.listSharedByMe(), getDrive: (id: string) => drivesApiFactory.getDrive(id), createDrive: (drive: Drive, options: any): AxiosPromise => drivesApiFactory.createDrive(drive, options), @@ -174,6 +187,9 @@ export const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => { groupApiFactory.addMember(groupId, { '@odata.id': `${server}graph/v1.0/users/${userId}` }), deleteMember: (groupId: string, userId: string) => groupApiFactory.deleteMember(groupId, userId) + }, + roleManagement: { + listPermissionRoleDefinitions: () => roleManagementApiFactory.listPermissionRoleDefinitions() } } } diff --git a/packages/web-client/src/helpers/share/functions.ts b/packages/web-client/src/helpers/share/functions.ts index 577be4b629a..73363a6eed6 100644 --- a/packages/web-client/src/helpers/share/functions.ts +++ b/packages/web-client/src/helpers/share/functions.ts @@ -12,17 +12,18 @@ import path from 'path' import { SHARE_JAIL_ID, SpaceResource, buildWebDavSpacesPath } from '../space' import { ShareStatus } from './status' import { SharePermissions } from './permission' -import { Share } from './share' import { buildSpaceShare } from './space' import { LinkShareRoles, PeopleShareRoles } from './role' -import { ShareResource } from './types' +import { ShareResource, Share } from './types' export const isShareResource = (resource: Resource): resource is ShareResource => { - return Object.hasOwn(resource, 'shareType') + return Object.hasOwn(resource, 'sharedWith') } /** * Transforms given shares into a resource format and returns only their unique occurences + * + * @deprecated */ export function aggregateResourceShares({ shares, @@ -133,6 +134,7 @@ function addMatchingSpaceToShares(shares, spaces) { return resources } +/** @deprecated */ export function buildSharedResource( share, incomingShares = false, @@ -155,7 +157,7 @@ export function buildSharedResource( path: undefined, webDavPath: undefined, processing: share.processing || false, - shareType: parseInt(share.share_type), + shareTypes: [parseInt(share.share_type)], owner: { id: share.uid_owner, displayName: share.displayname_owner }, sharedBy: { id: share.uid_owner, displayName: share.displayname_owner }, sharedWith: share.sharedWith || [] diff --git a/packages/web-client/src/helpers/share/functionsNG.ts b/packages/web-client/src/helpers/share/functionsNG.ts new file mode 100644 index 00000000000..1f8cdc5d8d0 --- /dev/null +++ b/packages/web-client/src/helpers/share/functionsNG.ts @@ -0,0 +1,190 @@ +import { extractDomSelector, extractExtensionFromFile, extractStorageId } from '../resource' +import { ShareTypes } from './type' +import { SHARE_JAIL_ID, buildWebDavSpacesPath } from '../space' +import { ShareStatus } from './status' +import { DriveItem, UnifiedRoleDefinition, User } from '../../generated' +import { GraphSharePermission, ShareResource } from './types' +import { urlJoin } from '../../utils' + +export const getShareResourceRoles = ({ + driveItem, + graphRoles +}: { + driveItem: DriveItem + graphRoles: UnifiedRoleDefinition[] +}) => { + return driveItem.remoteItem?.permissions.reduce((acc, permission) => { + permission.roles?.forEach((roleId) => { + const role = graphRoles.find(({ id }) => id === roleId) + if (role && !acc.some(({ id }) => id === role.id)) { + acc.push(role) + } + }) + + return acc + }, []) +} + +export const getShareResourcePermissions = ({ + driveItem, + shareRoles +}: { + driveItem: DriveItem + shareRoles: UnifiedRoleDefinition[] +}): GraphSharePermission[] => { + if (!shareRoles.length) { + // the server lists plain permissions if it doesn't find a corresponding role + const permissions = driveItem.remoteItem?.permissions.reduce( + (acc, permission) => { + const permissions = permission['@libre.graph.permissions.actions'] as GraphSharePermission[] + if (permissions) { + acc.push(...permissions) + } + + return acc + }, + [] + ) + return [...new Set(permissions)] + } + + const permissions = shareRoles.reduce((acc, role) => { + role.rolePermissions.forEach((permission) => { + acc.push(...permission.allowedResourceActions) + }) + return acc + }, []) + + return [...new Set(permissions)] +} + +export function buildIncomingShareResource({ + driveItem, + graphRoles +}: { + driveItem: DriveItem + graphRoles: UnifiedRoleDefinition[] +}): ShareResource { + const resourceName = driveItem.name || driveItem.remoteItem.name + const storageId = extractStorageId(driveItem.remoteItem.id) + const permission = driveItem.remoteItem?.permissions[0] + const id = permission?.id || driveItem.id + const shareType = permission?.grantedToV2.group ? ShareTypes.group.value : ShareTypes.user.value + + const shareRoles = getShareResourceRoles({ driveItem, graphRoles }) + const sharePermissions = getShareResourcePermissions({ driveItem, shareRoles }) + + const resource: ShareResource = { + id, + shareId: id, + path: '/', + name: resourceName, + fileId: driveItem.remoteItem.id, + storageId, + parentFolderId: driveItem.parentReference?.id, + sdate: driveItem.remoteItem.shared.sharedDateTime, + indicators: [], + tags: [], + webDavPath: buildWebDavSpacesPath([SHARE_JAIL_ID, id].join('!'), '/'), + sharedBy: driveItem.remoteItem.createdBy.user, + owner: driveItem.remoteItem.shared.owner.user, + sharedWith: [ + permission?.grantedToV2.group + ? { ...permission.grantedToV2.group, shareType } + : { ...permission?.grantedToV2.user, shareType } + ], + shareTypes: [shareType], + share: driveItem, + isFolder: !!driveItem.remoteItem.folder, + type: !!driveItem.remoteItem.folder ? 'folder' : 'file', + mimeType: driveItem.remoteItem.file?.mimeType || 'httpd/unix-directory', + status: permission?.['@client.synchronize'] ? ShareStatus.accepted : ShareStatus.declined, + syncEnabled: permission?.['@client.synchronize'], + hidden: permission?.['@ui.hidden'], + shareRoles, + sharePermissions, + canRename: () => !!permission?.['@client.synchronize'], + canDownload: () => sharePermissions.includes(GraphSharePermission.readBasic), + canUpload: () => sharePermissions.includes(GraphSharePermission.createUpload), + canCreate: () => sharePermissions.includes(GraphSharePermission.createChildren), + canBeDeleted: () => sharePermissions.includes(GraphSharePermission.deleteStandard), + canEditTags: () => sharePermissions.includes(GraphSharePermission.createChildren), + isMounted: () => false, + isReceivedShare: () => true, + canShare: () => false, + canDeny: () => false, // currently not possible with sharing NG (?) + getDomSelector: () => extractDomSelector(id) + } + + resource.extension = extractExtensionFromFile(resource) + + return resource +} + +export function buildOutgoingShareResource({ + driveItem, + user +}: { + driveItem: DriveItem + user: User +}): ShareResource { + const storageId = extractStorageId(driveItem.id) + const path = urlJoin(driveItem.parentReference.path, driveItem.name) + + const resource: ShareResource = { + id: driveItem.permissions[0].id, + shareId: driveItem.permissions[0].id, + path, + name: driveItem.name, + fileId: driveItem.id, + storageId, + parentFolderId: driveItem.parentReference?.id, + sdate: driveItem.lastModifiedDateTime, // FIXME: share date is missing in API + indicators: [], + tags: [], + webDavPath: buildWebDavSpacesPath(storageId, path), + sharedBy: { id: user.id, displayName: user.displayName }, + owner: { id: user.id, displayName: user.displayName }, + sharedWith: driveItem.permissions.map((p) => { + if (p.link) { + return { + id: p.id, + displayName: p.link['@libre.graph.displayName'], + shareType: ShareTypes.link.value + } + } + if (p.grantedToV2.group) { + return { ...p.grantedToV2.group, shareType: ShareTypes.group.value } + } + return { ...p.grantedToV2.user, shareType: ShareTypes.user.value } + }), + shareTypes: driveItem.permissions.map((p) => { + if (p.link) { + return ShareTypes.link.value + } + if (p.grantedToV2.group) { + return ShareTypes.group.value + } + return ShareTypes.user.value + }), + share: driveItem, + isFolder: !!driveItem.folder, + type: !!driveItem.folder ? 'folder' : 'file', + mimeType: driveItem.file?.mimeType || 'httpd/unix-directory', + canRename: () => true, + canDownload: () => true, + canUpload: () => true, + canCreate: () => true, + canBeDeleted: () => true, + canEditTags: () => true, + isMounted: () => false, + isReceivedShare: () => true, + canShare: () => true, + canDeny: () => true, + getDomSelector: () => extractDomSelector(driveItem.id) + } + + resource.extension = extractExtensionFromFile(resource) + + return resource +} diff --git a/packages/web-client/src/helpers/share/index.ts b/packages/web-client/src/helpers/share/index.ts index e7c182b7f4f..baf34f688ff 100644 --- a/packages/web-client/src/helpers/share/index.ts +++ b/packages/web-client/src/helpers/share/index.ts @@ -2,7 +2,6 @@ export * from './constants' export * from './functions' export * from './permission' export * from './role' -export * from './share' export * from './space' export * from './status' export * from './type' diff --git a/packages/web-client/src/helpers/share/share.ts b/packages/web-client/src/helpers/share/share.ts deleted file mode 100644 index def6173080e..00000000000 --- a/packages/web-client/src/helpers/share/share.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { User } from '../user' -import type { ShareRole } from './role' -import type { SharePermission } from './permission' - -export interface Share { - shareType: number - id: string - collaborator?: User - permissions?: number - role?: ShareRole - token?: string - url?: string - path?: string - description?: string - stime?: string - name?: string - password?: boolean - expiration?: string - itemSource?: string - file?: { parent: string; source: string; target: string } - owner?: User - fileOwner?: User - customPermissions?: SharePermission[] - expires?: Date - quicklink?: boolean - outgoing?: boolean - indirect?: boolean - notifyUploads?: boolean - notifyUploadsExtraRecipients?: string -} diff --git a/packages/web-client/src/helpers/share/space.ts b/packages/web-client/src/helpers/share/space.ts index fee5af4345f..f4c37635d36 100644 --- a/packages/web-client/src/helpers/share/space.ts +++ b/packages/web-client/src/helpers/share/space.ts @@ -1,5 +1,5 @@ import { spaceRoleEditor, spaceRoleManager, spaceRoleViewer } from './role' -import { Share } from './share' +import { Share } from './types' import { ShareTypes } from './type' export function buildSpaceShare(s, storageId): Share { diff --git a/packages/web-client/src/helpers/share/types.ts b/packages/web-client/src/helpers/share/types.ts index e84d697ae5c..b45ae931614 100644 --- a/packages/web-client/src/helpers/share/types.ts +++ b/packages/web-client/src/helpers/share/types.ts @@ -1,5 +1,8 @@ import { Identity, UnifiedRoleDefinition } from '../../generated' import { Resource } from '../resource' +import type { User } from '../user' +import type { ShareRole } from './role' +import type { SharePermission } from './permission' export enum GraphSharePermission { createUpload = 'libre.graph/driveItem/upload/create', @@ -17,15 +20,42 @@ export enum GraphSharePermission { } export interface ShareResource extends Resource { - shareType: number + sharedWith: Array<{ shareType: number } & Identity> + sharedBy: Identity syncEnabled?: boolean shareRoles?: UnifiedRoleDefinition[] sharePermissions?: GraphSharePermission[] - sharedWith?: Identity[] - sharedBy?: Identity hidden?: boolean status?: number // FIXME: remove in favour of syncEnabled share?: any // FIXME: type DriveItem OR remove? do we want to expose the share on each resource? } + +/** @deprecated */ +export interface Share { + shareType: number + id: string + collaborator?: User + permissions?: number + role?: ShareRole + token?: string + url?: string + path?: string + description?: string + stime?: string + name?: string + password?: boolean + expiration?: string + itemSource?: string + file?: { parent: string; source: string; target: string } + owner?: User + fileOwner?: User + customPermissions?: SharePermission[] + expires?: Date + quicklink?: boolean + outgoing?: boolean + indirect?: boolean + notifyUploads?: boolean + notifyUploadsExtraRecipients?: string +} diff --git a/packages/web-pkg/src/components/FilesList/ResourceTable.vue b/packages/web-pkg/src/components/FilesList/ResourceTable.vue index e83bb586d10..b2c6b20c316 100644 --- a/packages/web-pkg/src/components/FilesList/ResourceTable.vue +++ b/packages/web-pkg/src/components/FilesList/ResourceTable.vue @@ -828,10 +828,8 @@ export default defineComponent({ let panelToOpen if (file.type === 'space') { panelToOpen = 'space-share' - } else if (isShareResource(file) && file.shareType === ShareTypes.link.value) { - panelToOpen = 'sharing#linkShares' } else { - panelToOpen = 'sharing#peopleShares' + panelToOpen = 'sharing' } eventBus.publish(SideBarEventTopics.openWithPanel, panelToOpen) }, @@ -1022,35 +1020,38 @@ export default defineComponent({ if (!isShareResource(resource)) { return } - const resourceType = resource.type === 'folder' ? this.$gettext('folder') : this.$gettext('file') - if (!resource.sharedWith.length) { - return '' - } - - if (resource.shareType === ShareTypes.link.value) { - return this.$ngettext( - 'This %{ resourceType } is shared via %{ count } link', - 'This %{ resourceType } is shared via %{ count } links', - resource.sharedWith.length, - { - resourceType, - count: resource.sharedWith.length.toString() - } - ) - } + const shareCount = resource.sharedWith.filter(({ shareType }) => + [ShareTypes.user.value, ShareTypes.link.value].includes(shareType) + ).length + const linkCount = resource.sharedWith.filter( + ({ shareType }) => shareType === ShareTypes.link.value + ).length - return this.$ngettext( - 'This %{ resourceType } is shared via %{ count } invite', - 'This %{ resourceType } is shared via %{ count } invites', - resource.sharedWith.length, - { - resourceType, - count: resource.sharedWith.length.toString() - } - ) + const shareText = + shareCount > 0 + ? this.$ngettext( + 'This %{ resourceType } is shared via %{ shareCount } invite', + 'This %{ resourceType } is shared via %{ shareCount } invites', + shareCount + ) + : '' + const linkText = + linkCount > 0 + ? this.$ngettext( + 'This %{ resourceType } is shared via %{ linkCount } link', + 'This %{ resourceType } is shared via %{ linkCount } links', + linkCount + ) + : '' + const description = [shareText, linkText].join(' ') + return this.$gettext(description, { + resourceType, + shareCount: shareCount.toString(), + linkCount: linkCount.toString() + }) }, getOwnerAvatarDescription(resource: Resource) { const resourceType = @@ -1082,7 +1083,7 @@ export default defineComponent({ return resource.sharedWith.map((s) => ({ displayName: s.displayName, name: s.displayName, - shareType: resource.shareType, + shareType: s.shareType, username: s.id })) } @@ -1326,23 +1327,15 @@ export default defineComponent({ } // shared with me: on tablets hide shared with column and display owner column instead -#files-shared-with-me-pending-section .files-table .oc-table-header-cell-owner, -#files-shared-with-me-pending-section .files-table .oc-table-data-cell-owner, -#files-shared-with-me-accepted-section .files-table .oc-table-header-cell-owner, -#files-shared-with-me-accepted-section .files-table .oc-table-data-cell-owner, -#files-shared-with-me-declined-section .files-table .oc-table-header-cell-owner, -#files-shared-with-me-declined-section .files-table .oc-table-data-cell-owner { +#files-shared-with-me-view .files-table .oc-table-header-cell-owner, +#files-shared-with-me-view .files-table .oc-table-data-cell-owner { @media only screen and (min-width: 640px) { display: table-cell; } } -#files-shared-with-me-pending-section .files-table .oc-table-header-cell-sharedWith, -#files-shared-with-me-pending-section .files-table .oc-table-data-cell-sharedWith, -#files-shared-with-me-accepted-section .files-table .oc-table-header-cell-sharedWith, -#files-shared-with-me-accepted-section .files-table .oc-table-data-cell-sharedWith, -#files-shared-with-me-declined-section .files-table .oc-table-header-cell-sharedWith, -#files-shared-with-me-declined-section .files-table .oc-table-data-cell-sharedWith { +#files-shared-with-me-view .files-table .oc-table-header-cell-sharedWith, +#files-shared-with-me-view .files-table .oc-table-data-cell-sharedWith { @media only screen and (max-width: 1199px) { display: none; } diff --git a/packages/web-pkg/src/composables/piniaStores/shares/shares.ts b/packages/web-pkg/src/composables/piniaStores/shares/shares.ts index 465516727b7..2819054fc90 100644 --- a/packages/web-pkg/src/composables/piniaStores/shares/shares.ts +++ b/packages/web-pkg/src/composables/piniaStores/shares/shares.ts @@ -21,6 +21,7 @@ import { UpdateShareOptions } from './types' import { useResourcesStore } from '../resources' +import { UnifiedRoleDefinition } from '@ownclouders/web-client/src/generated' export const useSharesStore = defineStore('shares', () => { const configStore = useConfigStore() @@ -29,6 +30,7 @@ export const useSharesStore = defineStore('shares', () => { const loading = ref>() const shares = ref([]) as Ref + const graphRoles = ref([]) const incomingShares = computed(() => unref(shares).filter(({ outgoing }) => !outgoing) || []) const incomingCollaborators = computed( @@ -56,6 +58,10 @@ export const useSharesStore = defineStore('shares', () => { () => capabilityStore.sharingResharing && capabilityStore.sharingResharingDefault ) + const setGraphRoles = (values: UnifiedRoleDefinition[]) => { + graphRoles.value = values + } + const upsertShare = (share: Share) => { const existingShare = unref(shares).find(({ id }) => id === share.id) @@ -351,12 +357,15 @@ export const useSharesStore = defineStore('shares', () => { return { loading, shares, + graphRoles, incomingShares, incomingCollaborators, outgoingShares, outgoingLinks, outgoingCollaborators, + setGraphRoles, + pruneShares, loadShares, addShare, diff --git a/packages/web-pkg/src/composables/spaces/useGetMatchingSpace.ts b/packages/web-pkg/src/composables/spaces/useGetMatchingSpace.ts index a6b9416027a..3a040722367 100644 --- a/packages/web-pkg/src/composables/spaces/useGetMatchingSpace.ts +++ b/packages/web-pkg/src/composables/spaces/useGetMatchingSpace.ts @@ -52,7 +52,7 @@ export const useGetMatchingSpace = (options?: GetMatchingSpaceOptions) => { } const driveAliasPrefix = - (isShareResource(resource) && resource.shareType === ShareTypes.remote.value) || + (isShareResource(resource) && resource.shareTypes.includes(ShareTypes.remote.value)) || resource?.id?.toString().startsWith(OCM_PROVIDER_ID) ? 'ocm-share' : 'share' diff --git a/packages/web-pkg/src/helpers/share/triggerShareAction.ts b/packages/web-pkg/src/helpers/share/triggerShareAction.ts index 1cf9845fae2..fd9e407d0f5 100644 --- a/packages/web-pkg/src/helpers/share/triggerShareAction.ts +++ b/packages/web-pkg/src/helpers/share/triggerShareAction.ts @@ -31,7 +31,7 @@ export async function triggerShareAction({ throw new Error('invalid new share status') } - let action = `api/v1/shares/pending/${resource.share.id}` + let action = `api/v1/shares/pending/${resource.shareId}` if (hidden !== undefined) { action += `?hidden=${hidden ? 'true' : 'false'}` } diff --git a/packages/web-pkg/tests/unit/components/FilesList/ResourceTable.spec.ts b/packages/web-pkg/tests/unit/components/FilesList/ResourceTable.spec.ts index dba0f5aee87..fc7559b5443 100644 --- a/packages/web-pkg/tests/unit/components/FilesList/ResourceTable.spec.ts +++ b/packages/web-pkg/tests/unit/components/FilesList/ResourceTable.spec.ts @@ -103,6 +103,7 @@ const resourcesWithAllFields = [ ddate: getCurrentDate(), owner, sharedWith, + shareTypes: [], canRename: vi.fn, getDomSelector: () => extractDomSelector('forest') }, @@ -120,6 +121,7 @@ const resourcesWithAllFields = [ sdate: getCurrentDate(), ddate: getCurrentDate(), sharedWith, + shareTypes: [], owner, canRename: vi.fn, getDomSelector: () => extractDomSelector('notes') @@ -137,6 +139,7 @@ const resourcesWithAllFields = [ sdate: getCurrentDate(), ddate: getCurrentDate(), sharedWith, + shareTypes: [], owner, canRename: vi.fn, getDomSelector: () => extractDomSelector('documents') @@ -153,6 +156,7 @@ const resourcesWithAllFields = [ sdate: getCurrentDate(), ddate: getCurrentDate(), sharedWith, + shareTypes: [], tags: [], owner, canRename: vi.fn, diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts index 1dcd9bc14a8..893530e6f9d 100644 --- a/packages/web-runtime/src/index.ts +++ b/packages/web-runtime/src/index.ts @@ -43,6 +43,7 @@ import { createPinia } from 'pinia' import Avatar from './components/Avatar.vue' import focusMixin from './mixins/focusMixin' import { ArchiverService } from '@ownclouders/web-pkg' +import { UnifiedRoleDefinition } from '@ownclouders/web-client/src/generated' export const bootstrapApp = async (configurationPath: string): Promise => { const pinia = createPinia() @@ -57,7 +58,8 @@ export const bootstrapApp = async (configurationPath: string): Promise => extensionRegistry, spacesStore, userStore, - resourcesStore + resourcesStore, + sharesStore } = announcePiniaStores() app.provide('$router', router) @@ -204,6 +206,13 @@ export const bootstrapApp = async (configurationPath: string): Promise => field: 'name', value: app.config.globalProperties.$gettext('Personal') }) + + // load sharing roles from graph API + const { data } = + await clientService.graphAuthenticated.roleManagement.listPermissionRoleDefinitions() + + // FIXME: graph type is wrong + sharesStore.setGraphRoles(data as UnifiedRoleDefinition[]) }, { immediate: true diff --git a/tests/acceptance/expected-failures-with-ocis-server-ocis-storage.md b/tests/acceptance/expected-failures-with-ocis-server-ocis-storage.md index 396b890e84b..69c0010cf10 100644 --- a/tests/acceptance/expected-failures-with-ocis-server-ocis-storage.md +++ b/tests/acceptance/expected-failures-with-ocis-server-ocis-storage.md @@ -52,4 +52,4 @@ Other free text and markdown formatting can be used elsewhere in the document if - [webUIUpload/upload.feature:43](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUIUpload/upload.feature#L43) ### [PROPFIND to sub-folder of a shared resources with same name gives 404](https://github.com/owncloud/ocis/issues/3859) -- [webUISharingAcceptShares/acceptShares.feature:88](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUISharingAcceptShares/acceptShares.feature#L88) +- [webUISharingAcceptShares/acceptShares.feature:73](https://github.com/owncloud/web/blob/master/tests/acceptance/features/webUISharingAcceptShares/acceptShares.feature#L73) diff --git a/tests/acceptance/features/webUISharingAcceptShares/acceptShares.feature b/tests/acceptance/features/webUISharingAcceptShares/acceptShares.feature index 2422c05afac..8006536d460 100644 --- a/tests/acceptance/features/webUISharingAcceptShares/acceptShares.feature +++ b/tests/acceptance/features/webUISharingAcceptShares/acceptShares.feature @@ -11,21 +11,6 @@ Feature: accept/decline shares coming from internal users | Brian | And user "Brian" has logged in using the webUI - @issue-ocis-1950 - Scenario: reject a share that you received as user and as group member - Given these groups have been created in the server: - | groupname | - | grp1 | - And user "Alice" has created folder "/simple-folder" in the server - And user "Brian" has been added to group "grp1" in the server - And user "Alice" has shared folder "/simple-folder" with user "Brian" in the server - And user "Alice" has shared folder "/simple-folder" with group "grp1" in the server - And the user has browsed to the shared-with-me page - When the user declines share "simple-folder" offered by user "Alice Hansen" using the webUI - Then folder "simple-folder" shared by "Alice Hansen" should be in "Declined" state on the webUI - When the user browses to the files page - Then folder "/Shares" should not be listed on the webUI - Scenario: User receives files when auto accept share is disabled - oCIS behavior Given user "Alice" has created file "toshare.txt" in the server diff --git a/tests/e2e/cucumber/features/smoke/search/search.feature b/tests/e2e/cucumber/features/smoke/search/search.feature index a9ccc3e6172..c03cc5fdd0e 100644 --- a/tests/e2e/cucumber/features/smoke/search/search.feature +++ b/tests/e2e/cucumber/features/smoke/search/search.feature @@ -116,29 +116,8 @@ Feature: Search | resource | | strängéनेपालीName | - And "Alice" navigates to the shared with me page - When "Alice" reshares the following resource - | resource | recipient | type | role | resourceType | - | new_share_from_brian | Carol | user | Can view | folder | - | new-lorem-big.txt | Carol | user | Can view | file | And "Alice" logs out - # search re-shared resources - When "Carol" logs in - And "Carol" opens the "files" app - And "Carol" creates the following resources - | resource | type | - | folder | folder | - And "Carol" searches "NEW" using the global search and the "everywhere" filter - Then following resources should be displayed in the search list for user "Carol" - | resource | - | new_share_from_brian | - | new-lorem-big.txt | - But following resources should not be displayed in the search list for user "Carol" - | resource | - | folder | - And "Carol" logs out - Scenario: Search using "current folder" filter Given "Admin" creates following users using API diff --git a/tests/e2e/cucumber/features/smoke/shares/reshare.feature b/tests/e2e/cucumber/features/smoke/shares/reshare.feature deleted file mode 100644 index 429fbeb9dda..00000000000 --- a/tests/e2e/cucumber/features/smoke/shares/reshare.feature +++ /dev/null @@ -1,57 +0,0 @@ -Feature: reshare - - Scenario: re-sharing - Given "Admin" creates following users using API - | id | - | Alice | - | Brian | - | Carol | - And "Admin" creates following group using API - | id | - | sales | - | finance | - And "Admin" adds user to the group using API - | user | group | - | Carol | sales | - And "Alice" logs in - And "Alice" creates the following folder in personal space using API - | name | - | folder_to_shared | - And "Alice" shares the following resource using API - | resource | recipient | type | role | - | folder_to_shared | Brian | user | Can edit | - - And "Brian" logs in - And "Brian" opens the "files" app - And "Brian" navigates to the shared with me page - - # re-share with expiration date - And "Brian" reshares the following resource - | resource | recipient | type | role | resourceType | expirationDate | - | folder_to_shared | sales | group | Can view | folder | +5 days | - - And "Carol" logs in - And "Carol" opens the "files" app - And "Carol" navigates to the shared with me page - And "Carol" reshares the following resource - | resource | recipient | type | role | resourceType | - | folder_to_shared | Alice | user | Can view | folder | - - And "Alice" opens the "files" app - And "Alice" navigates to the personal space page - Then "Alice" should see the following recipients - | resource | recipient | type | role | - | folder_to_shared | Brian | user | can edit | - | folder_to_shared | sales | group | can view | - And "Alice" logs out - - When "Brian" updates following sharee role - | resource | recipient | type | role | resourceType | - | folder_to_shared | sales | group | custom_permissions:read | folder | - And "Brian" logs out - - And "Carol" navigates to the shared with me page - Then "Carol" should not be able to reshare the following resource - | resource | - | folder_to_shared | - And "Carol" logs out diff --git a/tests/e2e/cucumber/steps/ui/shares.ts b/tests/e2e/cucumber/steps/ui/shares.ts index 6eb2e122e9a..b63f22f7536 100644 --- a/tests/e2e/cucumber/steps/ui/shares.ts +++ b/tests/e2e/cucumber/steps/ui/shares.ts @@ -49,22 +49,6 @@ When( } ) -When( - '{string} reshares the following resource(s)', - async function (this: World, stepUser: string, stepTable: DataTable) { - const { page } = this.actorsEnvironment.getActor({ key: stepUser }) - const shareObject = new objects.applicationFiles.Share({ page }) - const shareInfo = parseShareTable(stepTable, this.usersEnvironment) - - for (const resource of Object.keys(shareInfo)) { - await shareObject.create({ - resource, - recipients: shareInfo[resource] - }) - } - } -) - When( '{string} accepts the following share(s)', async function (this: World, stepUser: string, stepTable: DataTable) { diff --git a/tests/e2e/support/objects/app-files/resource/actions.ts b/tests/e2e/support/objects/app-files/resource/actions.ts index 0d59e849bfe..cc7f86708c7 100644 --- a/tests/e2e/support/objects/app-files/resource/actions.ts +++ b/tests/e2e/support/objects/app-files/resource/actions.ts @@ -23,7 +23,7 @@ const checkBoxForTrashbin = `//*[@data-test-resource-path="%s"]//ancestor::tr//i export const fileRow = '//ancestor::*[(contains(@class, "oc-tile-card") or contains(@class, "oc-tbody-tr"))]' export const resourceNameSelector = - ':is(#files-files-table, .oc-tiles-item, #files-shared-with-me-accepted-section, .files-table) [data-test-resource-name="%s"]' + ':is(#files-files-table, .oc-tiles-item, .files-table) [data-test-resource-name="%s"]' const breadcrumbResourceNameSelector = '//span[contains(@class, "oc-breadcrumb-item-text") and text()="%s"]' const addNewResourceButton = `#new-file-menu-btn`