]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Allow admins to disable two factor auth
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
1 import express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { Hooks } from '@server/lib/plugins/hooks'
4 import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
5 import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
6 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
7 import {
8 isUserAdminFlagsValid,
9 isUserAutoPlayNextVideoValid,
10 isUserAutoPlayVideoValid,
11 isUserBlockedReasonValid,
12 isUserDescriptionValid,
13 isUserDisplayNameValid,
14 isUserNoModal,
15 isUserNSFWPolicyValid,
16 isUserP2PEnabledValid,
17 isUserPasswordValid,
18 isUserPasswordValidOrEmpty,
19 isUserRoleValid,
20 isUserUsernameValid,
21 isUserVideoLanguages,
22 isUserVideoQuotaDailyValid,
23 isUserVideoQuotaValid,
24 isUserVideosHistoryEnabledValid
25 } from '../../helpers/custom-validators/users'
26 import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
27 import { logger } from '../../helpers/logger'
28 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
29 import { Redis } from '../../lib/redis'
30 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
31 import { ActorModel } from '../../models/actor/actor'
32 import {
33 areValidationErrors,
34 checkUserEmailExist,
35 checkUserIdExist,
36 checkUserNameOrEmailDoesNotAlreadyExist,
37 doesVideoChannelIdExist,
38 doesVideoExist,
39 isValidVideoIdParam
40 } from './shared'
41
42 const usersListValidator = [
43 query('blocked')
44 .optional()
45 .customSanitizer(toBooleanOrNull)
46 .isBoolean().withMessage('Should be a valid blocked boolena'),
47
48 (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 if (areValidationErrors(req, res)) return
50
51 return next()
52 }
53 ]
54
55 const usersAddValidator = [
56 body('username')
57 .custom(isUserUsernameValid)
58 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
59 body('password')
60 .custom(isUserPasswordValidOrEmpty),
61 body('email')
62 .isEmail(),
63
64 body('channelName')
65 .optional()
66 .custom(isVideoChannelUsernameValid),
67
68 body('videoQuota')
69 .custom(isUserVideoQuotaValid),
70 body('videoQuotaDaily')
71 .custom(isUserVideoQuotaDailyValid),
72
73 body('role')
74 .customSanitizer(toIntOrNull)
75 .custom(isUserRoleValid),
76
77 body('adminFlags')
78 .optional()
79 .custom(isUserAdminFlagsValid),
80
81 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
82 if (areValidationErrors(req, res, { omitBodyLog: true })) return
83 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
84
85 const authUser = res.locals.oauth.token.User
86 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
87 return res.fail({
88 status: HttpStatusCode.FORBIDDEN_403,
89 message: 'You can only create users (and not administrators or moderators)'
90 })
91 }
92
93 if (req.body.channelName) {
94 if (req.body.channelName === req.body.username) {
95 return res.fail({ message: 'Channel name cannot be the same as user username.' })
96 }
97
98 const existing = await ActorModel.loadLocalByName(req.body.channelName)
99 if (existing) {
100 return res.fail({
101 status: HttpStatusCode.CONFLICT_409,
102 message: `Channel with name ${req.body.channelName} already exists.`
103 })
104 }
105 }
106
107 return next()
108 }
109 ]
110
111 const usersRegisterValidator = [
112 body('username')
113 .custom(isUserUsernameValid),
114 body('password')
115 .custom(isUserPasswordValid),
116 body('email')
117 .isEmail(),
118 body('displayName')
119 .optional()
120 .custom(isUserDisplayNameValid),
121
122 body('channel.name')
123 .optional()
124 .custom(isVideoChannelUsernameValid),
125 body('channel.displayName')
126 .optional()
127 .custom(isVideoChannelDisplayNameValid),
128
129 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
130 if (areValidationErrors(req, res, { omitBodyLog: true })) return
131 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
132
133 const body: UserRegister = req.body
134 if (body.channel) {
135 if (!body.channel.name || !body.channel.displayName) {
136 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
137 }
138
139 if (body.channel.name === body.username) {
140 return res.fail({ message: 'Channel name cannot be the same as user username.' })
141 }
142
143 const existing = await ActorModel.loadLocalByName(body.channel.name)
144 if (existing) {
145 return res.fail({
146 status: HttpStatusCode.CONFLICT_409,
147 message: `Channel with name ${body.channel.name} already exists.`
148 })
149 }
150 }
151
152 return next()
153 }
154 ]
155
156 const usersRemoveValidator = [
157 param('id')
158 .custom(isIdValid),
159
160 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
161 if (areValidationErrors(req, res)) return
162 if (!await checkUserIdExist(req.params.id, res)) return
163
164 const user = res.locals.user
165 if (user.username === 'root') {
166 return res.fail({ message: 'Cannot remove the root user' })
167 }
168
169 return next()
170 }
171 ]
172
173 const usersBlockingValidator = [
174 param('id')
175 .custom(isIdValid),
176 body('reason')
177 .optional()
178 .custom(isUserBlockedReasonValid),
179
180 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
181 if (areValidationErrors(req, res)) return
182 if (!await checkUserIdExist(req.params.id, res)) return
183
184 const user = res.locals.user
185 if (user.username === 'root') {
186 return res.fail({ message: 'Cannot block the root user' })
187 }
188
189 return next()
190 }
191 ]
192
193 const deleteMeValidator = [
194 (req: express.Request, res: express.Response, next: express.NextFunction) => {
195 const user = res.locals.oauth.token.User
196 if (user.username === 'root') {
197 return res.fail({ message: 'You cannot delete your root account.' })
198 }
199
200 return next()
201 }
202 ]
203
204 const usersUpdateValidator = [
205 param('id').custom(isIdValid),
206
207 body('password')
208 .optional()
209 .custom(isUserPasswordValid),
210 body('email')
211 .optional()
212 .isEmail(),
213 body('emailVerified')
214 .optional()
215 .isBoolean(),
216 body('videoQuota')
217 .optional()
218 .custom(isUserVideoQuotaValid),
219 body('videoQuotaDaily')
220 .optional()
221 .custom(isUserVideoQuotaDailyValid),
222 body('pluginAuth')
223 .optional()
224 .exists(),
225 body('role')
226 .optional()
227 .customSanitizer(toIntOrNull)
228 .custom(isUserRoleValid),
229 body('adminFlags')
230 .optional()
231 .custom(isUserAdminFlagsValid),
232
233 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
234 if (areValidationErrors(req, res, { omitBodyLog: true })) return
235 if (!await checkUserIdExist(req.params.id, res)) return
236
237 const user = res.locals.user
238 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
239 return res.fail({ message: 'Cannot change root role.' })
240 }
241
242 return next()
243 }
244 ]
245
246 const usersUpdateMeValidator = [
247 body('displayName')
248 .optional()
249 .custom(isUserDisplayNameValid),
250 body('description')
251 .optional()
252 .custom(isUserDescriptionValid),
253 body('currentPassword')
254 .optional()
255 .custom(isUserPasswordValid),
256 body('password')
257 .optional()
258 .custom(isUserPasswordValid),
259 body('email')
260 .optional()
261 .isEmail(),
262 body('nsfwPolicy')
263 .optional()
264 .custom(isUserNSFWPolicyValid),
265 body('autoPlayVideo')
266 .optional()
267 .custom(isUserAutoPlayVideoValid),
268 body('p2pEnabled')
269 .optional()
270 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
271 body('videoLanguages')
272 .optional()
273 .custom(isUserVideoLanguages),
274 body('videosHistoryEnabled')
275 .optional()
276 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
277 body('theme')
278 .optional()
279 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
280
281 body('noInstanceConfigWarningModal')
282 .optional()
283 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
284 body('noWelcomeModal')
285 .optional()
286 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
287 body('noAccountSetupWarningModal')
288 .optional()
289 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
290
291 body('autoPlayNextVideo')
292 .optional()
293 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
294
295 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
296 const user = res.locals.oauth.token.User
297
298 if (req.body.password || req.body.email) {
299 if (user.pluginAuth !== null) {
300 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
301 }
302
303 if (!req.body.currentPassword) {
304 return res.fail({ message: 'currentPassword parameter is missing.' })
305 }
306
307 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
308 return res.fail({
309 status: HttpStatusCode.UNAUTHORIZED_401,
310 message: 'currentPassword is invalid.'
311 })
312 }
313 }
314
315 if (areValidationErrors(req, res, { omitBodyLog: true })) return
316
317 return next()
318 }
319 ]
320
321 const usersGetValidator = [
322 param('id')
323 .custom(isIdValid),
324 query('withStats')
325 .optional()
326 .isBoolean().withMessage('Should have a valid withStats boolean'),
327
328 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
329 if (areValidationErrors(req, res)) return
330 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
331
332 return next()
333 }
334 ]
335
336 const usersVideoRatingValidator = [
337 isValidVideoIdParam('videoId'),
338
339 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
340 if (areValidationErrors(req, res)) return
341 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
342
343 return next()
344 }
345 ]
346
347 const usersVideosValidator = [
348 query('isLive')
349 .optional()
350 .customSanitizer(toBooleanOrNull)
351 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
352
353 query('channelId')
354 .optional()
355 .customSanitizer(toIntOrNull)
356 .custom(isIdValid),
357
358 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
359 if (areValidationErrors(req, res)) return
360
361 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
362
363 return next()
364 }
365 ]
366
367 const ensureUserRegistrationAllowed = [
368 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
369 const allowedParams = {
370 body: req.body,
371 ip: req.ip
372 }
373
374 const allowedResult = await Hooks.wrapPromiseFun(
375 isSignupAllowed,
376 allowedParams,
377 'filter:api.user.signup.allowed.result'
378 )
379
380 if (allowedResult.allowed === false) {
381 return res.fail({
382 status: HttpStatusCode.FORBIDDEN_403,
383 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
384 })
385 }
386
387 return next()
388 }
389 ]
390
391 const ensureUserRegistrationAllowedForIP = [
392 (req: express.Request, res: express.Response, next: express.NextFunction) => {
393 const allowed = isSignupAllowedForCurrentIP(req.ip)
394
395 if (allowed === false) {
396 return res.fail({
397 status: HttpStatusCode.FORBIDDEN_403,
398 message: 'You are not on a network authorized for registration.'
399 })
400 }
401
402 return next()
403 }
404 ]
405
406 const usersAskResetPasswordValidator = [
407 body('email')
408 .isEmail(),
409
410 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
411 if (areValidationErrors(req, res)) return
412
413 const exists = await checkUserEmailExist(req.body.email, res, false)
414 if (!exists) {
415 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
416 // Do not leak our emails
417 return res.status(HttpStatusCode.NO_CONTENT_204).end()
418 }
419
420 if (res.locals.user.pluginAuth) {
421 return res.fail({
422 status: HttpStatusCode.CONFLICT_409,
423 message: 'Cannot recover password of a user that uses a plugin authentication.'
424 })
425 }
426
427 return next()
428 }
429 ]
430
431 const usersResetPasswordValidator = [
432 param('id')
433 .custom(isIdValid),
434 body('verificationString')
435 .not().isEmpty(),
436 body('password')
437 .custom(isUserPasswordValid),
438
439 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
440 if (areValidationErrors(req, res)) return
441 if (!await checkUserIdExist(req.params.id, res)) return
442
443 const user = res.locals.user
444 const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
445
446 if (redisVerificationString !== req.body.verificationString) {
447 return res.fail({
448 status: HttpStatusCode.FORBIDDEN_403,
449 message: 'Invalid verification string.'
450 })
451 }
452
453 return next()
454 }
455 ]
456
457 const usersAskSendVerifyEmailValidator = [
458 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
459
460 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
461 if (areValidationErrors(req, res)) return
462
463 const exists = await checkUserEmailExist(req.body.email, res, false)
464 if (!exists) {
465 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
466 // Do not leak our emails
467 return res.status(HttpStatusCode.NO_CONTENT_204).end()
468 }
469
470 if (res.locals.user.pluginAuth) {
471 return res.fail({
472 status: HttpStatusCode.CONFLICT_409,
473 message: 'Cannot ask verification email of a user that uses a plugin authentication.'
474 })
475 }
476
477 return next()
478 }
479 ]
480
481 const usersVerifyEmailValidator = [
482 param('id')
483 .isInt().not().isEmpty().withMessage('Should have a valid id'),
484
485 body('verificationString')
486 .not().isEmpty().withMessage('Should have a valid verification string'),
487 body('isPendingEmail')
488 .optional()
489 .customSanitizer(toBooleanOrNull),
490
491 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
492 if (areValidationErrors(req, res)) return
493 if (!await checkUserIdExist(req.params.id, res)) return
494
495 const user = res.locals.user
496 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
497
498 if (redisVerificationString !== req.body.verificationString) {
499 return res.fail({
500 status: HttpStatusCode.FORBIDDEN_403,
501 message: 'Invalid verification string.'
502 })
503 }
504
505 return next()
506 }
507 ]
508
509 const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
510 return [
511 body('currentPassword').optional().custom(exists),
512
513 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
514 if (areValidationErrors(req, res)) return
515
516 const user = res.locals.oauth.token.User
517 const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
518 const targetUserId = parseInt(targetUserIdGetter(req) + '')
519
520 // Admin/moderator action on another user, skip the password check
521 if (isAdminOrModerator && targetUserId !== user.id) {
522 return next()
523 }
524
525 if (!req.body.currentPassword) {
526 return res.fail({
527 status: HttpStatusCode.BAD_REQUEST_400,
528 message: 'currentPassword is missing'
529 })
530 }
531
532 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
533 return res.fail({
534 status: HttpStatusCode.FORBIDDEN_403,
535 message: 'currentPassword is invalid.'
536 })
537 }
538
539 return next()
540 }
541 ]
542 }
543
544 const userAutocompleteValidator = [
545 param('search')
546 .isString()
547 .not().isEmpty()
548 ]
549
550 const ensureAuthUserOwnsAccountValidator = [
551 (req: express.Request, res: express.Response, next: express.NextFunction) => {
552 const user = res.locals.oauth.token.User
553
554 if (res.locals.account.id !== user.Account.id) {
555 return res.fail({
556 status: HttpStatusCode.FORBIDDEN_403,
557 message: 'Only owner of this account can access this resource.'
558 })
559 }
560
561 return next()
562 }
563 ]
564
565 const ensureCanManageChannelOrAccount = [
566 (req: express.Request, res: express.Response, next: express.NextFunction) => {
567 const user = res.locals.oauth.token.user
568 const account = res.locals.videoChannel?.Account ?? res.locals.account
569 const isUserOwner = account.userId === user.id
570
571 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
572 const message = `User ${user.username} does not have right this channel or account.`
573
574 return res.fail({
575 status: HttpStatusCode.FORBIDDEN_403,
576 message
577 })
578 }
579
580 return next()
581 }
582 ]
583
584 const ensureCanModerateUser = [
585 (req: express.Request, res: express.Response, next: express.NextFunction) => {
586 const authUser = res.locals.oauth.token.User
587 const onUser = res.locals.user
588
589 if (authUser.role === UserRole.ADMINISTRATOR) return next()
590 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
591
592 return res.fail({
593 status: HttpStatusCode.FORBIDDEN_403,
594 message: 'A moderator can only manage users.'
595 })
596 }
597 ]
598
599 // ---------------------------------------------------------------------------
600
601 export {
602 usersListValidator,
603 usersAddValidator,
604 deleteMeValidator,
605 usersRegisterValidator,
606 usersBlockingValidator,
607 usersRemoveValidator,
608 usersUpdateValidator,
609 usersUpdateMeValidator,
610 usersVideoRatingValidator,
611 usersCheckCurrentPasswordFactory,
612 ensureUserRegistrationAllowed,
613 ensureUserRegistrationAllowedForIP,
614 usersGetValidator,
615 usersVideosValidator,
616 usersAskResetPasswordValidator,
617 usersResetPasswordValidator,
618 usersAskSendVerifyEmailValidator,
619 usersVerifyEmailValidator,
620 userAutocompleteValidator,
621 ensureAuthUserOwnsAccountValidator,
622 ensureCanModerateUser,
623 ensureCanManageChannelOrAccount
624 }