aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/middlewares/validators/shared
diff options
context:
space:
mode:
authorWicklow <123956049+wickloww@users.noreply.github.com>2023-06-29 07:48:55 +0000
committerGitHub <noreply@github.com>2023-06-29 09:48:55 +0200
commit40346ead2b0b7afa475aef057d3673b6c7574b7a (patch)
tree24ffdc23c3a9d987334842e0d400b5bd44500cf7 /server/middlewares/validators/shared
parentae22c59f14d0d553f60b281948b6c232c2aca178 (diff)
downloadPeerTube-40346ead2b0b7afa475aef057d3673b6c7574b7a.tar.gz
PeerTube-40346ead2b0b7afa475aef057d3673b6c7574b7a.tar.zst
PeerTube-40346ead2b0b7afa475aef057d3673b6c7574b7a.zip
Feature/password protected videos (#5836)
* Add server endpoints * Refactoring test suites * Update server and add openapi documentation * fix compliation and tests * upload/import password protected video on client * add server error code * Add video password to update resolver * add custom message when sharing pw protected video * improve confirm component * Add new alert in component * Add ability to watch protected video on client * Cannot have password protected replay privacy * Add migration * Add tests * update after review * Update check params tests * Add live videos test * Add more filter test * Update static file privacy test * Update object storage tests * Add test on feeds * Add missing word * Fix tests * Fix tests on live videos * add embed support on password protected videos * fix style * Correcting data leaks * Unable to add password protected privacy on replay * Updated code based on review comments * fix validator and command * Updated code based on review comments
Diffstat (limited to 'server/middlewares/validators/shared')
-rw-r--r--server/middlewares/validators/shared/index.ts1
-rw-r--r--server/middlewares/validators/shared/video-passwords.ts80
-rw-r--r--server/middlewares/validators/shared/videos.ts85
3 files changed, 154 insertions, 12 deletions
diff --git a/server/middlewares/validators/shared/index.ts b/server/middlewares/validators/shared/index.ts
index de98cd442..e5cff2dda 100644
--- a/server/middlewares/validators/shared/index.ts
+++ b/server/middlewares/validators/shared/index.ts
@@ -10,4 +10,5 @@ export * from './video-comments'
10export * from './video-imports' 10export * from './video-imports'
11export * from './video-ownerships' 11export * from './video-ownerships'
12export * from './video-playlists' 12export * from './video-playlists'
13export * from './video-passwords'
13export * from './videos' 14export * from './videos'
diff --git a/server/middlewares/validators/shared/video-passwords.ts b/server/middlewares/validators/shared/video-passwords.ts
new file mode 100644
index 000000000..efcc95dc4
--- /dev/null
+++ b/server/middlewares/validators/shared/video-passwords.ts
@@ -0,0 +1,80 @@
1import express from 'express'
2import { HttpStatusCode, UserRight, VideoPrivacy } from '@shared/models'
3import { forceNumber } from '@shared/core-utils'
4import { VideoPasswordModel } from '@server/models/video/video-password'
5import { header } from 'express-validator'
6import { getVideoWithAttributes } from '@server/helpers/video'
7
8function isValidVideoPasswordHeader () {
9 return header('x-peertube-video-password')
10 .optional()
11 .isString()
12}
13
14function checkVideoIsPasswordProtected (res: express.Response) {
15 const video = getVideoWithAttributes(res)
16 if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED) {
17 res.fail({
18 status: HttpStatusCode.BAD_REQUEST_400,
19 message: 'Video is not password protected'
20 })
21 return false
22 }
23
24 return true
25}
26
27async function doesVideoPasswordExist (idArg: number | string, res: express.Response) {
28 const video = getVideoWithAttributes(res)
29 const id = forceNumber(idArg)
30 const videoPassword = await VideoPasswordModel.loadByIdAndVideo({ id, videoId: video.id })
31
32 if (!videoPassword) {
33 res.fail({
34 status: HttpStatusCode.NOT_FOUND_404,
35 message: 'Video password not found'
36 })
37 return false
38 }
39
40 res.locals.videoPassword = videoPassword
41
42 return true
43}
44
45async function isVideoPasswordDeletable (res: express.Response) {
46 const user = res.locals.oauth.token.User
47 const userAccount = user.Account
48 const video = res.locals.videoAll
49
50 // Check if the user who did the request is able to delete the video passwords
51 if (
52 user.hasRight(UserRight.UPDATE_ANY_VIDEO) === false && // Not a moderator
53 video.VideoChannel.accountId !== userAccount.id // Not the video owner
54 ) {
55 res.fail({
56 status: HttpStatusCode.FORBIDDEN_403,
57 message: 'Cannot remove passwords of another user\'s video'
58 })
59 return false
60 }
61
62 const passwordCount = await VideoPasswordModel.countByVideoId(video.id)
63
64 if (passwordCount <= 1) {
65 res.fail({
66 status: HttpStatusCode.BAD_REQUEST_400,
67 message: 'Cannot delete the last password of the protected video'
68 })
69 return false
70 }
71
72 return true
73}
74
75export {
76 isValidVideoPasswordHeader,
77 checkVideoIsPasswordProtected as isVideoPasswordProtected,
78 doesVideoPasswordExist,
79 isVideoPasswordDeletable
80}
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts
index 0033a32ff..9a7497007 100644
--- a/server/middlewares/validators/shared/videos.ts
+++ b/server/middlewares/validators/shared/videos.ts
@@ -20,6 +20,8 @@ import {
20 MVideoWithRights 20 MVideoWithRights
21} from '@server/types/models' 21} from '@server/types/models'
22import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models' 22import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models'
23import { VideoPasswordModel } from '@server/models/video/video-password'
24import { exists } from '@server/helpers/custom-validators/misc'
23 25
24async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { 26async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
25 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined 27 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
@@ -111,8 +113,12 @@ async function checkCanSeeVideo (options: {
111}) { 113}) {
112 const { req, res, video, paramId } = options 114 const { req, res, video, paramId } = options
113 115
114 if (video.requiresAuth({ urlParamId: paramId, checkBlacklist: true })) { 116 if (video.requiresUserAuth({ urlParamId: paramId, checkBlacklist: true })) {
115 return checkCanSeeAuthVideo(req, res, video) 117 return checkCanSeeUserAuthVideo({ req, res, video })
118 }
119
120 if (video.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
121 return checkCanSeePasswordProtectedVideo({ req, res, video })
116 } 122 }
117 123
118 if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) { 124 if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) {
@@ -122,7 +128,13 @@ async function checkCanSeeVideo (options: {
122 throw new Error('Unknown video privacy when checking video right ' + video.url) 128 throw new Error('Unknown video privacy when checking video right ' + video.url)
123} 129}
124 130
125async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights) { 131async function checkCanSeeUserAuthVideo (options: {
132 req: Request
133 res: Response
134 video: MVideoId | MVideoWithRights
135}) {
136 const { req, res, video } = options
137
126 const fail = () => { 138 const fail = () => {
127 res.fail({ 139 res.fail({
128 status: HttpStatusCode.FORBIDDEN_403, 140 status: HttpStatusCode.FORBIDDEN_403,
@@ -132,14 +144,12 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI
132 return false 144 return false
133 } 145 }
134 146
135 await authenticatePromise(req, res) 147 await authenticatePromise({ req, res })
136 148
137 const user = res.locals.oauth?.token.User 149 const user = res.locals.oauth?.token.User
138 if (!user) return fail() 150 if (!user) return fail()
139 151
140 const videoWithRights = (video as MVideoWithRights).VideoChannel?.Account?.userId 152 const videoWithRights = await getVideoWithRights(video as MVideoWithRights)
141 ? video as MVideoWithRights
142 : await VideoModel.loadFull(video.id)
143 153
144 const privacy = videoWithRights.privacy 154 const privacy = videoWithRights.privacy
145 155
@@ -148,16 +158,14 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI
148 return true 158 return true
149 } 159 }
150 160
151 const isOwnedByUser = videoWithRights.VideoChannel.Account.userId === user.id
152
153 if (videoWithRights.isBlacklisted()) { 161 if (videoWithRights.isBlacklisted()) {
154 if (isOwnedByUser || user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return true 162 if (canUserAccessVideo(user, videoWithRights, UserRight.MANAGE_VIDEO_BLACKLIST)) return true
155 163
156 return fail() 164 return fail()
157 } 165 }
158 166
159 if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) { 167 if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) {
160 if (isOwnedByUser || user.hasRight(UserRight.SEE_ALL_VIDEOS)) return true 168 if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true
161 169
162 return fail() 170 return fail()
163 } 171 }
@@ -166,6 +174,59 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI
166 return fail() 174 return fail()
167} 175}
168 176
177async function checkCanSeePasswordProtectedVideo (options: {
178 req: Request
179 res: Response
180 video: MVideo
181}) {
182 const { req, res, video } = options
183
184 const videoWithRights = await getVideoWithRights(video as MVideoWithRights)
185
186 const videoPassword = req.header('x-peertube-video-password')
187
188 if (!exists(videoPassword)) {
189 const errorMessage = 'Please provide a password to access this password protected video'
190 const errorType = ServerErrorCode.VIDEO_REQUIRES_PASSWORD
191
192 if (req.header('authorization')) {
193 await authenticatePromise({ req, res, errorMessage, errorStatus: HttpStatusCode.FORBIDDEN_403, errorType })
194 const user = res.locals.oauth?.token.User
195
196 if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true
197 }
198
199 res.fail({
200 status: HttpStatusCode.FORBIDDEN_403,
201 type: errorType,
202 message: errorMessage
203 })
204 return false
205 }
206
207 if (await VideoPasswordModel.isACorrectPassword({ videoId: video.id, password: videoPassword })) return true
208
209 res.fail({
210 status: HttpStatusCode.FORBIDDEN_403,
211 type: ServerErrorCode.INCORRECT_VIDEO_PASSWORD,
212 message: 'Incorrect video password. Access to the video is denied.'
213 })
214
215 return false
216}
217
218function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRight) {
219 const isOwnedByUser = video.VideoChannel.Account.userId === user.id
220
221 return isOwnedByUser || user.hasRight(right)
222}
223
224async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> {
225 return video.VideoChannel?.Account?.userId
226 ? video
227 : VideoModel.loadFull(video.id)
228}
229
169// --------------------------------------------------------------------------- 230// ---------------------------------------------------------------------------
170 231
171async function checkCanAccessVideoStaticFiles (options: { 232async function checkCanAccessVideoStaticFiles (options: {
@@ -176,7 +237,7 @@ async function checkCanAccessVideoStaticFiles (options: {
176}) { 237}) {
177 const { video, req, res } = options 238 const { video, req, res } = options
178 239
179 if (res.locals.oauth?.token.User) { 240 if (res.locals.oauth?.token.User || exists(req.header('x-peertube-video-password'))) {
180 return checkCanSeeVideo(options) 241 return checkCanSeeVideo(options)
181 } 242 }
182 243