diff options
author | Chocobozzz <me@florianbigard.com> | 2021-11-12 14:19:56 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-11-12 14:23:22 +0100 |
commit | 527a52ac4295a072927ff46761766a8b181a7603 (patch) | |
tree | 632f66b1691d8d72f04630671af8bdf1655d6b00 /server | |
parent | 8f2608e9a9d54c87ace636f99cdb9d2a7730990f (diff) | |
download | PeerTube-527a52ac4295a072927ff46761766a8b181a7603.tar.gz PeerTube-527a52ac4295a072927ff46761766a8b181a7603.tar.zst PeerTube-527a52ac4295a072927ff46761766a8b181a7603.zip |
Add ability to filter out public videos from admin
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/client.ts | 2 | ||||
-rw-r--r-- | server/helpers/query.ts | 1 | ||||
-rw-r--r-- | server/middlewares/validators/videos/videos.ts | 13 | ||||
-rw-r--r-- | server/models/video/sql/videos-id-list-query-builder.ts | 18 | ||||
-rw-r--r-- | server/models/video/video.ts | 15 | ||||
-rw-r--r-- | server/tests/api/check-params/videos-common-filters.ts | 22 | ||||
-rw-r--r-- | server/tests/api/videos/videos-common-filters.ts | 8 |
7 files changed, 63 insertions, 16 deletions
diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 0a27ace76..703166c01 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts | |||
@@ -10,7 +10,7 @@ import { HttpStatusCode } from '@shared/models' | |||
10 | import { root } from '../helpers/core-utils' | 10 | import { root } from '../helpers/core-utils' |
11 | import { STATIC_MAX_AGE } from '../initializers/constants' | 11 | import { STATIC_MAX_AGE } from '../initializers/constants' |
12 | import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html' | 12 | import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html' |
13 | import { asyncMiddleware, disableRobots, embedCSP } from '../middlewares' | 13 | import { asyncMiddleware, embedCSP } from '../middlewares' |
14 | 14 | ||
15 | const clientsRouter = express.Router() | 15 | const clientsRouter = express.Router() |
16 | 16 | ||
diff --git a/server/helpers/query.ts b/server/helpers/query.ts index 97bbdfc65..1142d02e4 100644 --- a/server/helpers/query.ts +++ b/server/helpers/query.ts | |||
@@ -16,6 +16,7 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) { | |||
16 | 'categoryOneOf', | 16 | 'categoryOneOf', |
17 | 'licenceOneOf', | 17 | 'licenceOneOf', |
18 | 'languageOneOf', | 18 | 'languageOneOf', |
19 | 'privacyOneOf', | ||
19 | 'tagsOneOf', | 20 | 'tagsOneOf', |
20 | 'tagsAllOf', | 21 | 'tagsAllOf', |
21 | 'isLocal', | 22 | 'isLocal', |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 53643635c..4916decbf 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -7,6 +7,7 @@ import { isAbleToUploadVideo } from '@server/lib/user' | |||
7 | import { getServerActor } from '@server/models/application/application' | 7 | import { getServerActor } from '@server/models/application/application' |
8 | import { ExpressPromiseHandler } from '@server/types/express' | 8 | import { ExpressPromiseHandler } from '@server/types/express' |
9 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' | 9 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' |
10 | import { getAllPrivacies } from '@shared/core-utils' | ||
10 | import { VideoInclude } from '@shared/models' | 11 | import { VideoInclude } from '@shared/models' |
11 | import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared' | 12 | import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared' |
12 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | 13 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' |
@@ -487,6 +488,10 @@ const commonVideosFiltersValidator = [ | |||
487 | .optional() | 488 | .optional() |
488 | .customSanitizer(toArray) | 489 | .customSanitizer(toArray) |
489 | .custom(isStringArray).withMessage('Should have a valid one of language array'), | 490 | .custom(isStringArray).withMessage('Should have a valid one of language array'), |
491 | query('privacyOneOf') | ||
492 | .optional() | ||
493 | .customSanitizer(toArray) | ||
494 | .custom(isNumberArray).withMessage('Should have a valid one of privacy array'), | ||
490 | query('tagsOneOf') | 495 | query('tagsOneOf') |
491 | .optional() | 496 | .optional() |
492 | .customSanitizer(toArray) | 497 | .customSanitizer(toArray) |
@@ -536,10 +541,12 @@ const commonVideosFiltersValidator = [ | |||
536 | // FIXME: deprecated in 4.0, to remove | 541 | // FIXME: deprecated in 4.0, to remove |
537 | { | 542 | { |
538 | if (req.query.filter === 'all-local') { | 543 | if (req.query.filter === 'all-local') { |
539 | req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY | 544 | req.query.include = VideoInclude.NOT_PUBLISHED_STATE |
540 | req.query.isLocal = true | 545 | req.query.isLocal = true |
546 | req.query.privacyOneOf = getAllPrivacies() | ||
541 | } else if (req.query.filter === 'all') { | 547 | } else if (req.query.filter === 'all') { |
542 | req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY | 548 | req.query.include = VideoInclude.NOT_PUBLISHED_STATE |
549 | req.query.privacyOneOf = getAllPrivacies() | ||
543 | } else if (req.query.filter === 'local') { | 550 | } else if (req.query.filter === 'local') { |
544 | req.query.isLocal = true | 551 | req.query.isLocal = true |
545 | } | 552 | } |
@@ -550,7 +557,7 @@ const commonVideosFiltersValidator = [ | |||
550 | const user = res.locals.oauth?.token.User | 557 | const user = res.locals.oauth?.token.User |
551 | 558 | ||
552 | if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) { | 559 | if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) { |
553 | if (req.query.include) { | 560 | if (req.query.include || req.query.privacyOneOf) { |
554 | return res.fail({ | 561 | return res.fail({ |
555 | status: HttpStatusCode.UNAUTHORIZED_401, | 562 | status: HttpStatusCode.UNAUTHORIZED_401, |
556 | message: 'You are not allowed to see all videos.' | 563 | message: 'You are not allowed to see all videos.' |
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts index 4a882e790..d825225ab 100644 --- a/server/models/video/sql/videos-id-list-query-builder.ts +++ b/server/models/video/sql/videos-id-list-query-builder.ts | |||
@@ -40,6 +40,7 @@ export type BuildVideosListQueryOptions = { | |||
40 | languageOneOf?: string[] | 40 | languageOneOf?: string[] |
41 | tagsOneOf?: string[] | 41 | tagsOneOf?: string[] |
42 | tagsAllOf?: string[] | 42 | tagsAllOf?: string[] |
43 | privacyOneOf?: VideoPrivacy[] | ||
43 | 44 | ||
44 | uuids?: string[] | 45 | uuids?: string[] |
45 | 46 | ||
@@ -138,11 +139,6 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
138 | this.whereStateAvailable() | 139 | this.whereStateAvailable() |
139 | } | 140 | } |
140 | 141 | ||
141 | // Only list videos with the appropriate priavcy | ||
142 | if (!(options.include & VideoInclude.HIDDEN_PRIVACY)) { | ||
143 | this.wherePrivacyAvailable(options.user) | ||
144 | } | ||
145 | |||
146 | if (options.videoPlaylistId) { | 142 | if (options.videoPlaylistId) { |
147 | this.joinPlaylist(options.videoPlaylistId) | 143 | this.joinPlaylist(options.videoPlaylistId) |
148 | } | 144 | } |
@@ -187,6 +183,13 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
187 | this.whereTagsAllOf(options.tagsAllOf) | 183 | this.whereTagsAllOf(options.tagsAllOf) |
188 | } | 184 | } |
189 | 185 | ||
186 | if (options.privacyOneOf) { | ||
187 | this.wherePrivacyOneOf(options.privacyOneOf) | ||
188 | } else { | ||
189 | // Only list videos with the appropriate priavcy | ||
190 | this.wherePrivacyAvailable(options.user) | ||
191 | } | ||
192 | |||
190 | if (options.uuids) { | 193 | if (options.uuids) { |
191 | this.whereUUIDs(options.uuids) | 194 | this.whereUUIDs(options.uuids) |
192 | } | 195 | } |
@@ -435,6 +438,11 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
435 | ) | 438 | ) |
436 | } | 439 | } |
437 | 440 | ||
441 | private wherePrivacyOneOf (privacyOneOf: VideoPrivacy[]) { | ||
442 | this.and.push('"video"."privacy" IN (:privacyOneOf)') | ||
443 | this.replacements.privacyOneOf = privacyOneOf | ||
444 | } | ||
445 | |||
438 | private whereUUIDs (uuids: string[]) { | 446 | private whereUUIDs (uuids: string[]) { |
439 | this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')') | 447 | this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')') |
440 | } | 448 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 003741da0..69d009e04 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1041,6 +1041,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1041 | languageOneOf?: string[] | 1041 | languageOneOf?: string[] |
1042 | tagsOneOf?: string[] | 1042 | tagsOneOf?: string[] |
1043 | tagsAllOf?: string[] | 1043 | tagsAllOf?: string[] |
1044 | privacyOneOf?: VideoPrivacy[] | ||
1044 | 1045 | ||
1045 | accountId?: number | 1046 | accountId?: number |
1046 | videoChannelId?: number | 1047 | videoChannelId?: number |
@@ -1059,6 +1060,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1059 | search?: string | 1060 | search?: string |
1060 | }) { | 1061 | }) { |
1061 | VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) | 1062 | VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) |
1063 | VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user) | ||
1062 | 1064 | ||
1063 | const trendingDays = options.sort.endsWith('trending') | 1065 | const trendingDays = options.sort.endsWith('trending') |
1064 | ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS | 1066 | ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS |
@@ -1082,6 +1084,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1082 | 'languageOneOf', | 1084 | 'languageOneOf', |
1083 | 'tagsOneOf', | 1085 | 'tagsOneOf', |
1084 | 'tagsAllOf', | 1086 | 'tagsAllOf', |
1087 | 'privacyOneOf', | ||
1085 | 'isLocal', | 1088 | 'isLocal', |
1086 | 'include', | 1089 | 'include', |
1087 | 'displayOnlyForFollower', | 1090 | 'displayOnlyForFollower', |
@@ -1119,6 +1122,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1119 | languageOneOf?: string[] | 1122 | languageOneOf?: string[] |
1120 | tagsOneOf?: string[] | 1123 | tagsOneOf?: string[] |
1121 | tagsAllOf?: string[] | 1124 | tagsAllOf?: string[] |
1125 | privacyOneOf?: VideoPrivacy[] | ||
1122 | 1126 | ||
1123 | displayOnlyForFollower: DisplayOnlyForFollowerOptions | null | 1127 | displayOnlyForFollower: DisplayOnlyForFollowerOptions | null |
1124 | 1128 | ||
@@ -1140,6 +1144,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1140 | uuids?: string[] | 1144 | uuids?: string[] |
1141 | }) { | 1145 | }) { |
1142 | VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) | 1146 | VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) |
1147 | VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user) | ||
1143 | 1148 | ||
1144 | const serverActor = await getServerActor() | 1149 | const serverActor = await getServerActor() |
1145 | 1150 | ||
@@ -1153,6 +1158,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1153 | 'languageOneOf', | 1158 | 'languageOneOf', |
1154 | 'tagsOneOf', | 1159 | 'tagsOneOf', |
1155 | 'tagsAllOf', | 1160 | 'tagsAllOf', |
1161 | 'privacyOneOf', | ||
1156 | 'user', | 1162 | 'user', |
1157 | 'isLocal', | 1163 | 'isLocal', |
1158 | 'host', | 1164 | 'host', |
@@ -1510,14 +1516,19 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1510 | 1516 | ||
1511 | private static throwIfPrivateIncludeWithoutUser (include: VideoInclude, user: MUserAccountId) { | 1517 | private static throwIfPrivateIncludeWithoutUser (include: VideoInclude, user: MUserAccountId) { |
1512 | if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) { | 1518 | if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) { |
1513 | throw new Error('Try to filter all-local but no user has not the see all videos right') | 1519 | throw new Error('Try to filter all-local but user cannot see all videos') |
1520 | } | ||
1521 | } | ||
1522 | |||
1523 | private static throwIfPrivacyOneOfWithoutUser (privacyOneOf: VideoPrivacy[], user: MUserAccountId) { | ||
1524 | if (privacyOneOf && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) { | ||
1525 | throw new Error('Try to choose video privacies but user cannot see all videos') | ||
1514 | } | 1526 | } |
1515 | } | 1527 | } |
1516 | 1528 | ||
1517 | private static isPrivateInclude (include: VideoInclude) { | 1529 | private static isPrivateInclude (include: VideoInclude) { |
1518 | return include & VideoInclude.BLACKLISTED || | 1530 | return include & VideoInclude.BLACKLISTED || |
1519 | include & VideoInclude.BLOCKED_OWNER || | 1531 | include & VideoInclude.BLOCKED_OWNER || |
1520 | include & VideoInclude.HIDDEN_PRIVACY || | ||
1521 | include & VideoInclude.NOT_PUBLISHED_STATE | 1532 | include & VideoInclude.NOT_PUBLISHED_STATE |
1522 | } | 1533 | } |
1523 | 1534 | ||
diff --git a/server/tests/api/check-params/videos-common-filters.ts b/server/tests/api/check-params/videos-common-filters.ts index afe42b0d5..f2b5bee8e 100644 --- a/server/tests/api/check-params/videos-common-filters.ts +++ b/server/tests/api/check-params/videos-common-filters.ts | |||
@@ -9,7 +9,7 @@ import { | |||
9 | setAccessTokensToServers, | 9 | setAccessTokensToServers, |
10 | setDefaultVideoChannel | 10 | setDefaultVideoChannel |
11 | } from '@shared/extra-utils' | 11 | } from '@shared/extra-utils' |
12 | import { HttpStatusCode, UserRole, VideoInclude } from '@shared/models' | 12 | import { HttpStatusCode, UserRole, VideoInclude, VideoPrivacy } from '@shared/models' |
13 | 13 | ||
14 | describe('Test video filters validators', function () { | 14 | describe('Test video filters validators', function () { |
15 | let server: PeerTubeServer | 15 | let server: PeerTubeServer |
@@ -112,7 +112,7 @@ describe('Test video filters validators', function () { | |||
112 | 112 | ||
113 | const validIncludes = [ | 113 | const validIncludes = [ |
114 | VideoInclude.NONE, | 114 | VideoInclude.NONE, |
115 | VideoInclude.HIDDEN_PRIVACY, | 115 | VideoInclude.BLOCKED_OWNER, |
116 | VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED | 116 | VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED |
117 | ] | 117 | ] |
118 | 118 | ||
@@ -120,6 +120,7 @@ describe('Test video filters validators', function () { | |||
120 | token?: string | 120 | token?: string |
121 | isLocal?: boolean | 121 | isLocal?: boolean |
122 | include?: VideoInclude | 122 | include?: VideoInclude |
123 | privacyOneOf?: VideoPrivacy[] | ||
123 | expectedStatus: HttpStatusCode | 124 | expectedStatus: HttpStatusCode |
124 | }) { | 125 | }) { |
125 | const paths = [ | 126 | const paths = [ |
@@ -136,6 +137,7 @@ describe('Test video filters validators', function () { | |||
136 | token: options.token || server.accessToken, | 137 | token: options.token || server.accessToken, |
137 | query: { | 138 | query: { |
138 | isLocal: options.isLocal, | 139 | isLocal: options.isLocal, |
140 | privacyOneOf: options.privacyOneOf, | ||
139 | include: options.include | 141 | include: options.include |
140 | }, | 142 | }, |
141 | expectedStatus: options.expectedStatus | 143 | expectedStatus: options.expectedStatus |
@@ -143,6 +145,22 @@ describe('Test video filters validators', function () { | |||
143 | } | 145 | } |
144 | } | 146 | } |
145 | 147 | ||
148 | it('Should fail with a bad privacyOneOf', async function () { | ||
149 | await testEndpoints({ privacyOneOf: [ 'toto' ] as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
150 | }) | ||
151 | |||
152 | it('Should succeed with a good privacyOneOf', async function () { | ||
153 | await testEndpoints({ privacyOneOf: [ VideoPrivacy.INTERNAL ], expectedStatus: HttpStatusCode.OK_200 }) | ||
154 | }) | ||
155 | |||
156 | it('Should fail to use privacyOneOf with a simple user', async function () { | ||
157 | await testEndpoints({ | ||
158 | privacyOneOf: [ VideoPrivacy.INTERNAL ], | ||
159 | token: userAccessToken, | ||
160 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
161 | }) | ||
162 | }) | ||
163 | |||
146 | it('Should fail with a bad include', async function () { | 164 | it('Should fail with a bad include', async function () { |
147 | await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 165 | await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
148 | }) | 166 | }) |
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts index 4f22d4ac3..ca5f42173 100644 --- a/server/tests/api/videos/videos-common-filters.ts +++ b/server/tests/api/videos/videos-common-filters.ts | |||
@@ -138,6 +138,7 @@ describe('Test videos filter', function () { | |||
138 | hasWebtorrentFiles?: boolean | 138 | hasWebtorrentFiles?: boolean |
139 | hasHLSFiles?: boolean | 139 | hasHLSFiles?: boolean |
140 | include?: VideoInclude | 140 | include?: VideoInclude |
141 | privacyOneOf?: VideoPrivacy[] | ||
141 | category?: number | 142 | category?: number |
142 | tagsAllOf?: string[] | 143 | tagsAllOf?: string[] |
143 | token?: string | 144 | token?: string |
@@ -148,7 +149,7 @@ describe('Test videos filter', function () { | |||
148 | path: options.path, | 149 | path: options.path, |
149 | token: options.token ?? options.server.accessToken, | 150 | token: options.token ?? options.server.accessToken, |
150 | query: { | 151 | query: { |
151 | ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles' ]), | 152 | ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles', 'privacyOneOf' ]), |
152 | 153 | ||
153 | sort: 'createdAt' | 154 | sort: 'createdAt' |
154 | }, | 155 | }, |
@@ -162,6 +163,7 @@ describe('Test videos filter', function () { | |||
162 | server: PeerTubeServer | 163 | server: PeerTubeServer |
163 | isLocal?: boolean | 164 | isLocal?: boolean |
164 | include?: VideoInclude | 165 | include?: VideoInclude |
166 | privacyOneOf?: VideoPrivacy[] | ||
165 | token?: string | 167 | token?: string |
166 | expectedStatus?: HttpStatusCode | 168 | expectedStatus?: HttpStatusCode |
167 | }) { | 169 | }) { |
@@ -195,7 +197,7 @@ describe('Test videos filter', function () { | |||
195 | server, | 197 | server, |
196 | token, | 198 | token, |
197 | isLocal: true, | 199 | isLocal: true, |
198 | include: VideoInclude.HIDDEN_PRIVACY | 200 | privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ] |
199 | }) | 201 | }) |
200 | 202 | ||
201 | for (const names of namesResults) { | 203 | for (const names of namesResults) { |
@@ -216,7 +218,7 @@ describe('Test videos filter', function () { | |||
216 | const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({ | 218 | const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({ |
217 | server, | 219 | server, |
218 | token, | 220 | token, |
219 | include: VideoInclude.HIDDEN_PRIVACY | 221 | privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ] |
220 | }) | 222 | }) |
221 | 223 | ||
222 | expect(channelVideos).to.have.lengthOf(3) | 224 | expect(channelVideos).to.have.lengthOf(3) |