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
75 changes: 51 additions & 24 deletions packages/web-runtime/src/container/sse/shares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
buildIncomingShareResource,
buildOutgoingShareResource,
isShareSpaceResource,
ShareTypes
ShareTypes,
SpaceResource
} from '@opencloud-eu/web-client'
import {
eventBus,
Expand Down Expand Up @@ -50,6 +51,35 @@ function isCurrentSpaceAffected({
return resourcesStore.currentFolder?.storageId === sseData.spaceid
}

// resolves to undefined if the current user can't access the space anymore. fetching the single drive
// isn't enough because it ignores the membership for users who are allowed to list all spaces
async function getRemainingSpace({
id,
clientService
}: { id: string } & Pick<SSEEventOptions, 'clientService'>) {
try {
const [space] = await clientService.graphAuthenticated.drives.listMyDrives({
filter: `id eq '${id}'`
})
return space
} catch {
return undefined
}
}

async function upsertSpaceWithPermissions({
space,
spacesStore,
clientService
}: { space: SpaceResource } & Pick<SSEEventOptions, 'spacesStore' | 'clientService'>) {
spacesStore.upsertSpace(space)
await spacesStore.loadGraphPermissions({
ids: [space.id],
graphClient: clientService.graphAuthenticated,
useCache: false
})
}

export const onSSESpaceMemberAddedEvent = async ({
sseData,
resourcesStore,
Expand All @@ -63,12 +93,7 @@ export const onSSESpaceMemberAddedEvent = async ({
}

const space = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid)
spacesStore.upsertSpace(space)
await spacesStore.loadGraphPermissions({
ids: [space.id],
graphClient: clientService.graphAuthenticated,
useCache: false
})
await upsertSpaceWithPermissions({ space, spacesStore, clientService })

if (!isLocationSpacesActive(router, 'files-spaces-projects')) {
return
Expand All @@ -95,12 +120,7 @@ export const onSSESpaceMemberRemovedEvent = async ({

if (!isCurrentUserAffected({ sseData, authStore, userStore })) {
const space = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid)
spacesStore.upsertSpace(space)
await spacesStore.loadGraphPermissions({
ids: [space.id],
graphClient: clientService.graphAuthenticated,
useCache: false
})
await upsertSpaceWithPermissions({ space, spacesStore, clientService })
return
}

Expand All @@ -109,15 +129,27 @@ export const onSSESpaceMemberRemovedEvent = async ({
return
}

// the user may still have access to the space, e.g. via a group membership
const space = await getRemainingSpace({ id: sseData.itemid, clientService })
const isInRemovedSpace =
isLocationSpacesActive(router, 'files-spaces-generic') &&
removedSpace.id === resourcesStore.currentFolder?.storageId

if (space) {
await upsertSpaceWithPermissions({ space, spacesStore, clientService })

if (isInRemovedSpace) {
// the role might have changed
eventBus.publish('app.files.list.load')
}
return
}

const { $gettext } = language

spacesStore.removeSpace(removedSpace, { deleted: false })

if (
isLocationSpacesActive(router, 'files-spaces-generic') &&
removedSpace.id === resourcesStore.currentFolder?.storageId
) {
// Fixme: Message triggers when user membership was revoked, but still is in a group membership, which is wrong
if (isInRemovedSpace) {
return messageStore.showMessage({
title: $gettext(
'Your access to this space has been revoked. Please navigate to another location.'
Expand Down Expand Up @@ -145,12 +177,7 @@ export const onSSESpaceShareUpdatedEvent = async ({
}

const space = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid)
spacesStore.upsertSpace(space)
await spacesStore.loadGraphPermissions({
ids: [space.id],
graphClient: clientService.graphAuthenticated,
useCache: false
})
await upsertSpaceWithPermissions({ space, spacesStore, clientService })

if (
isLocationSpacesActive(router, 'files-spaces-generic') &&
Expand Down
53 changes: 48 additions & 5 deletions packages/web-runtime/tests/unit/container/sse/shares.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,37 +88,79 @@ describe('shares events', () => {
expect(mocks.spacesStore.removeSpace).not.toHaveBeenCalled()
expect(mocks.messageStore.showMessage).not.toHaveBeenCalled()
})
it('calls "removeSpace" when space member has been removed and current user is affected', async () => {
it('calls "removeSpace" when space member has been removed and current user has no access anymore', async () => {
const space = mock<SpaceResource>({ id: 'space1' })
const mocks = getMocks({ currentRouteFilesSpacesProjects: true, spaces: [space] })
const sseData = mock<EventSchemaType>({
itemid: space.id,
spaceid: space.id,
affecteduserids: ['1']
})
// users who may list all spaces still get the single drive after losing their membership
mocks.clientService.graphAuthenticated.drives.getDrive.mockResolvedValue(space)
mocks.clientService.graphAuthenticated.drives.listMyDrives.mockResolvedValue([])
await onSSESpaceMemberRemovedEvent({ sseData, ...mocks })
expect(mocks.clientService.graphAuthenticated.drives.getDrive).not.toHaveBeenCalled()
expect(mocks.clientService.graphAuthenticated.drives.listMyDrives).toHaveBeenCalledWith({
filter: `id eq '${space.id}'`
})
expect(mocks.spacesStore.upsertSpace).not.toHaveBeenCalled()
// the space itself still exists, e.g. for the admin settings
expect(mocks.spacesStore.removeSpace).toHaveBeenCalledWith(space, { deleted: false })
expect(mocks.resourcesStore.removeResources).toHaveBeenCalledWith([space])
expect(mocks.messageStore.showMessage).not.toHaveBeenCalled()
})
it('calls "showMessage" when space member has been removed and current user is affected and navigated to space', async () => {
it('calls "showMessage" when space member has been removed and current user has no access anymore and navigated to space', async () => {
const space = mock<SpaceResource>({ id: 'space1' })
const mocks = getMocks({ currentRouteFilesSpacesGeneric: true, spaces: [space] })
const sseData = mock<EventSchemaType>({
itemid: space.id,
spaceid: space.id,
affecteduserids: ['1']
})
mocks.clientService.graphAuthenticated.drives.getDrive.mockResolvedValue(space)
mocks.clientService.graphAuthenticated.drives.listMyDrives.mockRejectedValue(new Error('500'))
await onSSESpaceMemberRemovedEvent({ sseData, ...mocks })
expect(mocks.clientService.graphAuthenticated.drives.getDrive).not.toHaveBeenCalled()
expect(mocks.spacesStore.upsertSpace).not.toHaveBeenCalled()
expect(mocks.spacesStore.removeSpace).toHaveBeenCalled()
expect(mocks.messageStore.showMessage).toHaveBeenCalled()
})
describe('current user is affected but still has access, e.g. via a group', () => {
it('keeps the space and reloads its permissions', async () => {
const space = mock<SpaceResource>({ id: 'space1' })
const mocks = getMocks({ currentRouteFilesSpacesProjects: true, spaces: [space] })
const sseData = mock<EventSchemaType>({
itemid: space.id,
spaceid: space.id,
affecteduserids: ['1']
})
mocks.clientService.graphAuthenticated.drives.listMyDrives.mockResolvedValue([space])
await onSSESpaceMemberRemovedEvent({ sseData, ...mocks })
expect(mocks.clientService.graphAuthenticated.drives.listMyDrives).toHaveBeenCalledWith({
filter: `id eq '${space.id}'`
})
expect(mocks.spacesStore.upsertSpace).toHaveBeenCalledWith(space)
expect(mocks.spacesStore.loadGraphPermissions).toHaveBeenCalledWith(
expect.objectContaining({ ids: [space.id], useCache: false })
)
expect(mocks.spacesStore.removeSpace).not.toHaveBeenCalled()
expect(mocks.resourcesStore.removeResources).not.toHaveBeenCalled()
expect(mocks.messageStore.showMessage).not.toHaveBeenCalled()
})
it('reloads the file list instead of showing a message when navigated to space', async () => {
const space = mock<SpaceResource>({ id: 'space1' })
const mocks = getMocks({ currentRouteFilesSpacesGeneric: true, spaces: [space] })
const sseData = mock<EventSchemaType>({
itemid: space.id,
spaceid: space.id,
affecteduserids: ['1']
})
mocks.clientService.graphAuthenticated.drives.listMyDrives.mockResolvedValue([space])
const busStub = vi.spyOn(eventBus, 'publish')
await onSSESpaceMemberRemovedEvent({ sseData, ...mocks })
expect(mocks.spacesStore.removeSpace).not.toHaveBeenCalled()
expect(mocks.messageStore.showMessage).not.toHaveBeenCalled()
expect(busStub).toHaveBeenCalledWith('app.files.list.load')
})
})
it('does not trigger any action when initiator ids are identical', async () => {
const space = mock<SpaceResource>({ id: 'space1' })
const mocks = getMocks({ currentRouteFilesSpacesProjects: true, spaces: [space] })
Expand All @@ -131,6 +173,7 @@ describe('shares events', () => {
mocks.clientService.graphAuthenticated.drives.getDrive.mockResolvedValue(space)
await onSSESpaceMemberRemovedEvent({ sseData, ...mocks })
expect(mocks.clientService.graphAuthenticated.drives.getDrive).not.toHaveBeenCalled()
expect(mocks.clientService.graphAuthenticated.drives.listMyDrives).not.toHaveBeenCalled()
expect(mocks.spacesStore.upsertSpace).not.toHaveBeenCalled()
expect(mocks.spacesStore.removeSpace).not.toHaveBeenCalled()
expect(mocks.messageStore.showMessage).not.toHaveBeenCalled()
Expand Down
Loading