Refactor video rights checker

pull/5074/head
Chocobozzz 2022-06-22 09:44:08 +02:00
parent 2e401e8575
commit ff9d43f62a
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
8 changed files with 90 additions and 74 deletions

View File

@ -6,6 +6,7 @@ import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helper
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { import {
areValidationErrors, areValidationErrors,
checkCanSeeVideo,
doesAccountIdExist, doesAccountIdExist,
doesAccountNameWithHostExist, doesAccountNameWithHostExist,
doesUserFeedTokenCorrespond, doesUserFeedTokenCorrespond,
@ -112,7 +113,10 @@ const videoCommentsFeedsValidator = [
return res.fail({ message: 'videoId cannot be mixed with a channel filter' }) return res.fail({ message: 'videoId cannot be mixed with a channel filter' })
} }
if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return if (req.query.videoId) {
if (!await doesVideoExist(req.query.videoId, res)) return
if (!await checkCanSeeVideo({ req, res, paramId: req.query.videoId, video: res.locals.videoAll })) return
}
return next() return next()
} }

View File

@ -1,4 +1,5 @@
import { Request, Response } from 'express' import { Request, Response } from 'express'
import { isUUIDValid } from '@server/helpers/custom-validators/misc'
import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' import { loadVideo, VideoLoadType } from '@server/lib/model-loaders'
import { isAbleToUploadVideo } from '@server/lib/user' import { isAbleToUploadVideo } from '@server/lib/user'
import { authenticatePromiseIfNeeded } from '@server/middlewares/auth' import { authenticatePromiseIfNeeded } from '@server/middlewares/auth'
@ -18,18 +19,19 @@ import {
MVideoThumbnail, MVideoThumbnail,
MVideoWithRights MVideoWithRights
} from '@server/types/models' } from '@server/types/models'
import { HttpStatusCode, ServerErrorCode, UserRight } from '@shared/models' import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models'
async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
const video = await loadVideo(id, fetchType, userId) const video = await loadVideo(id, fetchType, userId)
if (video === null) { if (!video) {
res.fail({ res.fail({
status: HttpStatusCode.NOT_FOUND_404, status: HttpStatusCode.NOT_FOUND_404,
message: 'Video not found' message: 'Video not found'
}) })
return false return false
} }
@ -58,6 +60,8 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
return true return true
} }
// ---------------------------------------------------------------------------
async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) { async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) { if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
res.fail({ res.fail({
@ -70,6 +74,8 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st
return true return true
} }
// ---------------------------------------------------------------------------
async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
@ -95,32 +101,77 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
return true return true
} }
async function checkCanSeeVideoIfPrivate (req: Request, res: Response, video: MVideo, authenticateInQuery = false) { // ---------------------------------------------------------------------------
if (!video.requiresAuth()) return true
const videoWithRights = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id) async function checkCanSeeVideo (options: {
req: Request
res: Response
paramId: string
video: MVideo
authenticateInQuery?: boolean // default false
}) {
const { req, res, video, paramId, authenticateInQuery = false } = options
return checkCanSeePrivateVideo(req, res, videoWithRights, authenticateInQuery) if (video.requiresAuth()) {
return checkCanSeeAuthVideo(req, res, video, authenticateInQuery)
}
if (video.privacy === VideoPrivacy.UNLISTED) {
if (isUUIDValid(paramId)) return true
return checkCanSeeAuthVideo(req, res, video, authenticateInQuery)
}
if (video.privacy === VideoPrivacy.PUBLIC) return true
throw new Error('Fatal error when checking video right ' + video.url)
} }
async function checkCanSeePrivateVideo (req: Request, res: Response, video: MVideoWithRights, authenticateInQuery = false) { async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights, authenticateInQuery = false) {
await authenticatePromiseIfNeeded(req, res, authenticateInQuery) const fail = () => {
const user = res.locals.oauth ? res.locals.oauth.token.User : null
// Only the owner or a user that have blocklist rights can see the video
if (!user || !user.canGetVideo(video)) {
res.fail({ res.fail({
status: HttpStatusCode.FORBIDDEN_403, status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot fetch information of private/internal/blocklisted video' message: 'Cannot fetch information of private/internal/blocked video'
}) })
return false return false
} }
return true await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
const user = res.locals.oauth?.token.User
if (!user) return fail()
const videoWithRights = (video as MVideoWithRights).VideoChannel?.Account?.userId
? video as MVideoWithRights
: await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)
const privacy = videoWithRights.privacy
if (privacy === VideoPrivacy.INTERNAL) {
// We know we have a user
return true
}
const isOwnedByUser = videoWithRights.VideoChannel.Account.userId === user.id
if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) {
if (isOwnedByUser && user.hasRight(UserRight.SEE_ALL_VIDEOS)) return true
return fail()
}
if (videoWithRights.isBlacklisted()) {
if (isOwnedByUser || user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return true
return fail()
}
// Should not happen
return fail()
} }
// ---------------------------------------------------------------------------
function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) {
// Retrieve the user who did the request // Retrieve the user who did the request
if (onlyOwned && video.isOwned() === false) { if (onlyOwned && video.isOwned() === false) {
@ -146,6 +197,8 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right:
return true return true
} }
// ---------------------------------------------------------------------------
async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) { async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) {
if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
res.fail({ res.fail({
@ -167,7 +220,6 @@ export {
doesVideoFileOfVideoExist, doesVideoFileOfVideoExist,
checkUserCanManageVideo, checkUserCanManageVideo,
checkCanSeeVideoIfPrivate, checkCanSeeVideo,
checkCanSeePrivateVideo,
checkUserQuota checkUserQuota
} }

View File

@ -7,7 +7,7 @@ import { logger } from '../../../helpers/logger'
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants' import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants'
import { import {
areValidationErrors, areValidationErrors,
checkCanSeeVideoIfPrivate, checkCanSeeVideo,
checkUserCanManageVideo, checkUserCanManageVideo,
doesVideoCaptionExist, doesVideoCaptionExist,
doesVideoExist, doesVideoExist,
@ -74,7 +74,7 @@ const listVideoCaptionsValidator = [
if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
const video = res.locals.onlyVideo const video = res.locals.onlyVideo
if (!await checkCanSeeVideoIfPrivate(req, res, video)) return if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.videoId })) return
return next() return next()
} }

View File

@ -10,7 +10,7 @@ import { Hooks } from '../../../lib/plugins/hooks'
import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
import { import {
areValidationErrors, areValidationErrors,
checkCanSeeVideoIfPrivate, checkCanSeeVideo,
doesVideoCommentExist, doesVideoCommentExist,
doesVideoCommentThreadExist, doesVideoCommentThreadExist,
doesVideoExist, doesVideoExist,
@ -54,7 +54,7 @@ const listVideoCommentThreadsValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
return next() return next()
} }
@ -73,7 +73,7 @@ const listVideoThreadCommentsValidator = [
if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
return next() return next()
} }
@ -91,7 +91,7 @@ const addVideoCommentThreadValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (!await doesVideoExist(req.params.videoId, res)) return if (!await doesVideoExist(req.params.videoId, res)) return
if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return
@ -113,7 +113,7 @@ const addVideoCommentReplyValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (!await doesVideoExist(req.params.videoId, res)) return if (!await doesVideoExist(req.params.videoId, res)) return
if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return

View File

@ -8,7 +8,7 @@ import { isRatingValid } from '../../../helpers/custom-validators/video-rates'
import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { areValidationErrors, checkCanSeeVideoIfPrivate, doesVideoExist, isValidVideoIdParam } from '../shared' import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam } from '../shared'
const videoUpdateRateValidator = [ const videoUpdateRateValidator = [
isValidVideoIdParam('id'), isValidVideoIdParam('id'),
@ -21,7 +21,7 @@ const videoUpdateRateValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (!await doesVideoExist(req.params.id, res)) return if (!await doesVideoExist(req.params.id, res)) return
if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return if (!await checkCanSeeVideo({ req, res, paramId: req.params.id, video: res.locals.videoAll })) return
return next() return next()
} }

View File

@ -7,14 +7,13 @@ import { getServerActor } from '@server/models/application/application'
import { ExpressPromiseHandler } from '@server/types/express-handler' import { ExpressPromiseHandler } from '@server/types/express-handler'
import { MUserAccountId, MVideoFullLight } from '@server/types/models' import { MUserAccountId, MVideoFullLight } from '@server/types/models'
import { getAllPrivacies } from '@shared/core-utils' import { getAllPrivacies } from '@shared/core-utils'
import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoPrivacy } from '@shared/models' import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude } from '@shared/models'
import { import {
exists, exists,
isBooleanValid, isBooleanValid,
isDateValid, isDateValid,
isFileValid, isFileValid,
isIdValid, isIdValid,
isUUIDValid,
toArray, toArray,
toBooleanOrNull, toBooleanOrNull,
toIntOrNull, toIntOrNull,
@ -50,7 +49,7 @@ import { Hooks } from '../../../lib/plugins/hooks'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { import {
areValidationErrors, areValidationErrors,
checkCanSeePrivateVideo, checkCanSeeVideo,
checkUserCanManageVideo, checkUserCanManageVideo,
checkUserQuota, checkUserQuota,
doesVideoChannelOfAccountExist, doesVideoChannelOfAccountExist,
@ -297,28 +296,9 @@ const videosCustomGetValidator = (
const video = getVideoWithAttributes(res) as MVideoFullLight const video = getVideoWithAttributes(res) as MVideoFullLight
// Video private or blacklisted if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id, authenticateInQuery })) return
if (video.requiresAuth()) {
if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) {
return next()
}
return return next()
}
// Video is public, anyone can access it
if (video.privacy === VideoPrivacy.PUBLIC) return next()
// Video is unlisted, check we used the uuid to fetch it
if (video.privacy === VideoPrivacy.UNLISTED) {
if (isUUIDValid(req.params.id)) return next()
// Don't leak this unlisted video
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video not found'
})
}
} }
] ]
} }

View File

@ -29,12 +29,11 @@ import {
MUserDefault, MUserDefault,
MUserFormattable, MUserFormattable,
MUserNotifSettingChannelDefault, MUserNotifSettingChannelDefault,
MUserWithNotificationSetting, MUserWithNotificationSetting
MVideoWithRights
} from '@server/types/models' } from '@server/types/models'
import { AttributesOnly } from '@shared/typescript-utils' import { AttributesOnly } from '@shared/typescript-utils'
import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users'
import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models'
import { User, UserRole } from '../../../shared/models/users' import { User, UserRole } from '../../../shared/models/users'
import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
@ -851,22 +850,6 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
.then(u => u.map(u => u.username)) .then(u => u.map(u => u.username))
} }
canGetVideo (video: MVideoWithRights) {
const videoUserId = video.VideoChannel.Account.userId
if (video.isBlacklisted()) {
return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
}
if (video.privacy === VideoPrivacy.PRIVATE) {
return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
}
if (video.privacy === VideoPrivacy.INTERNAL) return true
return false
}
hasRight (right: UserRight) { hasRight (right: UserRight) {
return hasUserRight(this.role, right) return hasUserRight(this.role, right)
} }

View File

@ -39,10 +39,7 @@ describe('Test videos API validator', function () {
await setAccessTokensToServers([ server ]) await setAccessTokensToServers([ server ])
const username = 'user1' userAccessToken = await server.users.generateUserAndToken('user1')
const password = 'my super password'
await server.users.create({ username: username, password: password })
userAccessToken = await server.login.getAccessToken({ username, password })
{ {
const body = await server.users.getMyInfo() const body = await server.users.getMyInfo()