mirror of
https://github.com/immich-app/immich.git
synced 2026-03-12 21:42:54 -07:00
fix(server): restrict individual shared link asset removal to owners (#26868)
* fix(server): restrict individual shared link asset removal to owners * make open-api
This commit is contained in:
@@ -438,6 +438,16 @@ describe('/shared-links', () => {
|
|||||||
expect(body).toEqual(errorDto.badRequest('Invalid shared link type'));
|
expect(body).toEqual(errorDto.badRequest('Invalid shared link type'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reject guests removing assets from an individual shared link', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.delete(`/shared-links/${linkWithAssets.id}/assets`)
|
||||||
|
.query({ key: linkWithAssets.key })
|
||||||
|
.send({ assetIds: [asset1.id] });
|
||||||
|
|
||||||
|
expect(status).toBe(403);
|
||||||
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
|
});
|
||||||
|
|
||||||
it('should remove assets from a shared link (individual)', async () => {
|
it('should remove assets from a shared link (individual)', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/shared-links/${linkWithAssets.id}/assets`)
|
.delete(`/shared-links/${linkWithAssets.id}/assets`)
|
||||||
|
|||||||
@@ -12,15 +12,18 @@ import { asBearerAuth, utils } from 'src/utils';
|
|||||||
test.describe('Shared Links', () => {
|
test.describe('Shared Links', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let asset: AssetMediaResponseDto;
|
let asset: AssetMediaResponseDto;
|
||||||
|
let asset2: AssetMediaResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
let sharedLink: SharedLinkResponseDto;
|
let sharedLink: SharedLinkResponseDto;
|
||||||
let sharedLinkPassword: SharedLinkResponseDto;
|
let sharedLinkPassword: SharedLinkResponseDto;
|
||||||
|
let individualSharedLink: SharedLinkResponseDto;
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
utils.initSdk();
|
utils.initSdk();
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
asset = await utils.createAsset(admin.accessToken);
|
asset = await utils.createAsset(admin.accessToken);
|
||||||
|
asset2 = await utils.createAsset(admin.accessToken);
|
||||||
album = await createAlbum(
|
album = await createAlbum(
|
||||||
{
|
{
|
||||||
createAlbumDto: {
|
createAlbumDto: {
|
||||||
@@ -39,6 +42,10 @@ test.describe('Shared Links', () => {
|
|||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
password: 'test-password',
|
password: 'test-password',
|
||||||
});
|
});
|
||||||
|
individualSharedLink = await utils.createSharedLink(admin.accessToken, {
|
||||||
|
type: SharedLinkType.Individual,
|
||||||
|
assetIds: [asset.id, asset2.id],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('download from a shared link', async ({ page }) => {
|
test('download from a shared link', async ({ page }) => {
|
||||||
@@ -109,4 +116,21 @@ test.describe('Shared Links', () => {
|
|||||||
await page.waitForURL('/photos');
|
await page.waitForURL('/photos');
|
||||||
await page.locator(`[data-asset-id="${asset.id}"]`).waitFor();
|
await page.locator(`[data-asset-id="${asset.id}"]`).waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('owner can remove assets from an individual shared link', async ({ context, page }) => {
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
|
await page.goto(`/share/${individualSharedLink.key}`);
|
||||||
|
await page.locator(`[data-asset="${asset.id}"]`).waitFor();
|
||||||
|
await expect(page.locator(`[data-asset]`)).toHaveCount(2);
|
||||||
|
|
||||||
|
await page.locator(`[data-asset="${asset.id}"]`).hover();
|
||||||
|
await page.locator(`[data-asset="${asset.id}"] [role="checkbox"]`).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Remove from shared link' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Remove', exact: true }).click();
|
||||||
|
|
||||||
|
await expect(page.locator(`[data-asset="${asset.id}"]`)).toHaveCount(0);
|
||||||
|
await expect(page.locator(`[data-asset="${asset2.id}"]`)).toHaveCount(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
21
mobile/openapi/lib/api/shared_links_api.dart
generated
21
mobile/openapi/lib/api/shared_links_api.dart
generated
@@ -427,11 +427,7 @@ class SharedLinksApi {
|
|||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
///
|
///
|
||||||
/// * [AssetIdsDto] assetIdsDto (required):
|
/// * [AssetIdsDto] assetIdsDto (required):
|
||||||
///
|
Future<Response> removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async {
|
||||||
/// * [String] key:
|
|
||||||
///
|
|
||||||
/// * [String] slug:
|
|
||||||
Future<Response> removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async {
|
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/shared-links/{id}/assets'
|
final apiPath = r'/shared-links/{id}/assets'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@@ -443,13 +439,6 @@ class SharedLinksApi {
|
|||||||
final headerParams = <String, String>{};
|
final headerParams = <String, String>{};
|
||||||
final formParams = <String, String>{};
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
if (key != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
|
||||||
}
|
|
||||||
if (slug != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'slug', slug));
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentTypes = <String>['application/json'];
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
@@ -473,12 +462,8 @@ class SharedLinksApi {
|
|||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
///
|
///
|
||||||
/// * [AssetIdsDto] assetIdsDto (required):
|
/// * [AssetIdsDto] assetIdsDto (required):
|
||||||
///
|
Future<List<AssetIdsResponseDto>?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto,) async {
|
||||||
/// * [String] key:
|
final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto,);
|
||||||
///
|
|
||||||
/// * [String] slug:
|
|
||||||
Future<List<AssetIdsResponseDto>?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async {
|
|
||||||
final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, slug: slug, );
|
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11605,22 +11605,6 @@
|
|||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "key",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "slug",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@@ -11677,6 +11661,7 @@
|
|||||||
"state": "Stable"
|
"state": "Stable"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"x-immich-permission": "sharedLink.update",
|
||||||
"x-immich-state": "Stable"
|
"x-immich-state": "Stable"
|
||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
|
|||||||
@@ -5987,19 +5987,14 @@ export function updateSharedLink({ id, sharedLinkEditDto }: {
|
|||||||
/**
|
/**
|
||||||
* Remove assets from a shared link
|
* Remove assets from a shared link
|
||||||
*/
|
*/
|
||||||
export function removeSharedLinkAssets({ id, key, slug, assetIdsDto }: {
|
export function removeSharedLinkAssets({ id, assetIdsDto }: {
|
||||||
id: string;
|
id: string;
|
||||||
key?: string;
|
|
||||||
slug?: string;
|
|
||||||
assetIdsDto: AssetIdsDto;
|
assetIdsDto: AssetIdsDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: AssetIdsResponseDto[];
|
data: AssetIdsResponseDto[];
|
||||||
}>(`/shared-links/${encodeURIComponent(id)}/assets${QS.query(QS.explode({
|
}>(`/shared-links/${encodeURIComponent(id)}/assets`, oazapfts.json({
|
||||||
key,
|
|
||||||
slug
|
|
||||||
}))}`, oazapfts.json({
|
|
||||||
...opts,
|
...opts,
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: assetIdsDto
|
body: assetIdsDto
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
||||||
import { SharedLinkType } from 'src/enum';
|
import { Permission, SharedLinkType } from 'src/enum';
|
||||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
describe(SharedLinkController.name, () => {
|
describe(SharedLinkController.name, () => {
|
||||||
@@ -31,4 +32,16 @@ describe(SharedLinkController.name, () => {
|
|||||||
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ expiresAt: null }));
|
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ expiresAt: null }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('DELETE /shared-links/:id/assets', () => {
|
||||||
|
it('should require shared link update permission', async () => {
|
||||||
|
await request(ctx.getHttpServer()).delete(`/shared-links/${factory.uuid()}/assets`).send({ assetIds: [] });
|
||||||
|
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
metadata: expect.objectContaining({ permission: Permission.SharedLinkUpdate, sharedLinkRoute: false }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export class SharedLinkController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id/assets')
|
@Delete(':id/assets')
|
||||||
@Authenticated({ sharedLink: true })
|
@Authenticated({ permission: Permission.SharedLinkUpdate })
|
||||||
@Endpoint({
|
@Endpoint({
|
||||||
summary: 'Remove assets from a shared link',
|
summary: 'Remove assets from a shared link',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
|
||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||||
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
||||||
@@ -138,7 +137,6 @@ export const handleRemoveSharedLinkAssets = async (sharedLink: SharedLinkRespons
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await removeSharedLinkAssets({
|
const results = await removeSharedLinkAssets({
|
||||||
...authManager.params,
|
|
||||||
id: sharedLink.id,
|
id: sharedLink.id,
|
||||||
assetIdsDto: { assetIds },
|
assetIdsDto: { assetIds },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user