Skip to content

Commit

Permalink
Check video rights before providing AP information
Browse files Browse the repository at this point in the history
  • Loading branch information
Chocobozzz committed Apr 26, 2024
1 parent b8635c2 commit afb2827
Show file tree
Hide file tree
Showing 31 changed files with 247 additions and 226 deletions.
11 changes: 9 additions & 2 deletions packages/tests/src/api/moderation/video-blacklist.ts
Expand Up @@ -3,12 +3,12 @@
import { expect } from 'chai'
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
import { sortObjectComparator } from '@peertube/peertube-core-utils'
import { UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models'
import { HttpStatusCode, UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models'
import {
BlacklistCommand,
cleanupTests,
createMultipleServers,
doubleFollow, PeerTubeServer,
doubleFollow, makeActivityPubGetRequest, PeerTubeServer,
setAccessTokensToServers,
setDefaultChannelAvatar,
waitJobs
Expand Down Expand Up @@ -298,6 +298,13 @@ describe('Test video blacklist', function () {
expect(video4Blacklisted.unfederated).to.be.true
})

it('Should not have AP comments/announces/likes/dislikes', async function () {
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/comments`, HttpStatusCode.UNAUTHORIZED_401)
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/announces`, HttpStatusCode.UNAUTHORIZED_401)
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/likes`, HttpStatusCode.UNAUTHORIZED_401)
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/dislikes`, HttpStatusCode.UNAUTHORIZED_401)
})

it('Should remove the video from blacklist and refederate the video', async function () {
await command.remove({ videoId: video4UUID })

Expand Down
18 changes: 9 additions & 9 deletions server/core/controllers/activitypub/client.ts
Expand Up @@ -120,7 +120,7 @@ activityPubClientRouter.get('/videos/watch/:id/activity',
activityPubClientRouter.get('/videos/watch/:id/announces',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoAnnouncesController)
)
activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
Expand All @@ -132,19 +132,19 @@ activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
activityPubClientRouter.get('/videos/watch/:id/likes',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoLikesController)
)
activityPubClientRouter.get('/videos/watch/:id/dislikes',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoDislikesController)
)
activityPubClientRouter.get('/videos/watch/:id/comments',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoCommentsController)
)
activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
Expand Down Expand Up @@ -175,7 +175,7 @@ activityPubClientRouter.get('/videos/watch/:id/chapters',
activityPubRateLimiter,
apVideoChaptersSetCacheKey,
chaptersCacheRouteMiddleware(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
asyncMiddleware(videosCustomGetValidator('only-video')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoChaptersController)
)

Expand Down Expand Up @@ -330,7 +330,7 @@ async function videoAnnounceController (req: express.Request, res: express.Respo
}

async function videoAnnouncesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyImmutableVideo
const video = res.locals.onlyVideo

if (redirectIfNotOwned(video.url, res)) return

Expand All @@ -347,7 +347,7 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
}

async function videoLikesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyImmutableVideo
const video = res.locals.onlyVideo

if (redirectIfNotOwned(video.url, res)) return

Expand All @@ -357,7 +357,7 @@ async function videoLikesController (req: express.Request, res: express.Response
}

async function videoDislikesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyImmutableVideo
const video = res.locals.onlyVideo

if (redirectIfNotOwned(video.url, res)) return

Expand All @@ -367,7 +367,7 @@ async function videoDislikesController (req: express.Request, res: express.Respo
}

async function videoCommentsController (req: express.Request, res: express.Response) {
const video = res.locals.onlyImmutableVideo
const video = res.locals.onlyVideo

if (redirectIfNotOwned(video.url, res)) return

Expand Down
2 changes: 1 addition & 1 deletion server/core/controllers/api/videos/chapters.ts
Expand Up @@ -11,7 +11,7 @@ import { replaceChapters } from '@server/lib/video-chapters.js'
const videoChaptersRouter = express.Router()

videoChaptersRouter.get('/:id/chapters',
asyncMiddleware(videosCustomGetValidator('only-video')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(listVideoChapters)
)

Expand Down
7 changes: 4 additions & 3 deletions server/core/controllers/api/videos/ownership.ts
@@ -1,6 +1,7 @@
import express from 'express'
import { HttpStatusCode, VideoChangeOwnershipStatus, VideoState } from '@peertube/peertube-models'
import { HttpStatusCode, VideoChangeOwnershipStatus } from '@peertube/peertube-models'
import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
import { MVideoFullLight } from '@server/types/models/index.js'
import express from 'express'
import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
Expand Down Expand Up @@ -113,7 +114,7 @@ function acceptOwnership (req: express.Request, res: express.Response) {
const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight
targetVideoUpdated.VideoChannel = channel

if (targetVideoUpdated.hasPrivacyForFederation() && targetVideoUpdated.state === VideoState.PUBLISHED) {
if (canVideoBeFederated(targetVideoUpdated)) {
await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t)
await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor)
}
Expand Down
2 changes: 1 addition & 1 deletion server/core/controllers/api/videos/token.ts
Expand Up @@ -7,7 +7,7 @@ const tokenRouter = express.Router()

tokenRouter.post('/:id/token',
optionalAuthenticate,
asyncMiddleware(videosCustomGetValidator('only-video')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
videoFileTokenValidator,
generateToken
)
Expand Down
19 changes: 10 additions & 9 deletions server/core/controllers/api/videos/update.ts
@@ -1,17 +1,21 @@
import express, { UploadFiles } from 'express'
import { Transaction } from 'sequelize'
import { forceNumber } from '@peertube/peertube-core-utils'
import { HttpStatusCode, ThumbnailType, VideoPrivacy, VideoPrivacyType, VideoUpdate } from '@peertube/peertube-models'
import { exists } from '@server/helpers/custom-validators/misc.js'
import { changeVideoChannelShare } from '@server/lib/activitypub/share.js'
import { isNewVideoPrivacyForFederation, isPrivacyForFederation } from '@server/lib/activitypub/videos/federate.js'
import { updateLocalVideoMiniatureFromExisting } from '@server/lib/thumbnail.js'
import { replaceChaptersFromDescriptionIfNeeded } from '@server/lib/video-chapters.js'
import { addVideoJobsAfterUpdate } from '@server/lib/video-jobs.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { setVideoPrivacy } from '@server/lib/video-privacy.js'
import { setVideoTags } from '@server/lib/video.js'
import { openapiOperationDoc } from '@server/middlewares/doc.js'
import { VideoPasswordModel } from '@server/models/video/video-password.js'
import { FilteredModelAttributes } from '@server/types/index.js'
import { MVideoFullLight, MVideoThumbnail } from '@server/types/models/index.js'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js'
import express, { UploadFiles } from 'express'
import { Transaction } from 'sequelize'
import { VideoAuditView, auditLoggerFactory, getAuditIdFromRes } from '../../../helpers/audit-logger.js'
import { resetSequelizeInstance } from '../../../helpers/database-utils.js'
import { createReqFiles } from '../../../helpers/express-utils.js'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
Expand All @@ -22,9 +26,6 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist.js'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares/index.js'
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update.js'
import { VideoModel } from '../../../models/video/video.js'
import { replaceChaptersFromDescriptionIfNeeded } from '@server/lib/video-chapters.js'
import { addVideoJobsAfterUpdate } from '@server/lib/video-jobs.js'
import { updateLocalVideoMiniatureFromExisting } from '@server/lib/thumbnail.js'

const lTags = loggerTagsFactory('api', 'video')
const auditLogger = auditLoggerFactory('videos')
Expand Down Expand Up @@ -53,7 +54,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON())
const videoInfoToUpdate: VideoUpdate = req.body

const hadPrivacyForFederation = videoFromReq.hasPrivacyForFederation()
const hadPrivacyForFederation = isPrivacyForFederation(videoFromReq.privacy)
const oldPrivacy = videoFromReq.privacy

const thumbnails = await buildVideoThumbnailsFromReq(videoFromReq, req.files)
Expand Down Expand Up @@ -191,7 +192,7 @@ async function updateVideoPrivacy (options: {
transaction: Transaction
}) {
const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options
const isNewVideoForFederation = videoInstance.isNewVideoForFederation(videoInfoToUpdate.privacy)
const isNewVideoForFederation = isNewVideoPrivacyForFederation(videoInfoToUpdate.privacy, videoInfoToUpdate.privacy)

const newPrivacy = forceNumber(videoInfoToUpdate.privacy) as VideoPrivacyType
setVideoPrivacy(videoInstance, newPrivacy)
Expand All @@ -207,7 +208,7 @@ async function updateVideoPrivacy (options: {
}

// Unfederate the video if the new privacy is not compatible with federation
if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
if (hadPrivacyForFederation && !isPrivacyForFederation(videoInstance.privacy)) {
await VideoModel.sendDelete(videoInstance, { transaction })
}

Expand Down
35 changes: 6 additions & 29 deletions server/core/helpers/video.ts
@@ -1,51 +1,28 @@
import { Response } from 'express'
import { forceNumber } from '@peertube/peertube-core-utils'
import { VideoPrivacy, VideoPrivacyType, VideoState, VideoStateType } from '@peertube/peertube-models'
import { VideoPrivacy } from '@peertube/peertube-models'
import { CONFIG } from '@server/initializers/config.js'
import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/types/models/index.js'
import { Response } from 'express'

function getVideoWithAttributes (res: Response) {
export function getVideoWithAttributes (res: Response) {
return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo
}

function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
export function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
return isStreamingPlaylist(videoOrPlaylist)
? videoOrPlaylist.Video
: videoOrPlaylist
}

function isPrivacyForFederation (privacy: VideoPrivacyType) {
const castedPrivacy = forceNumber(privacy)

return castedPrivacy === VideoPrivacy.PUBLIC ||
(CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED)
}

function isStateForFederation (state: VideoStateType) {
const castedState = forceNumber(state)

return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED
}

function getPrivaciesForFederation () {
export function getPrivaciesForFederation () {
return (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true)
? [ { privacy: VideoPrivacy.PUBLIC }, { privacy: VideoPrivacy.UNLISTED } ]
: [ { privacy: VideoPrivacy.PUBLIC } ]
}

function getExtFromMimetype (mimeTypes: { [id: string]: string | string[] }, mimeType: string) {
export function getExtFromMimetype (mimeTypes: { [id: string]: string | string[] }, mimeType: string) {
const value = mimeTypes[mimeType]

if (Array.isArray(value)) return value[0]

return value
}

export {
getVideoWithAttributes,
extractVideo,
getExtFromMimetype,
isStateForFederation,
isPrivacyForFederation,
getPrivaciesForFederation
}
2 changes: 1 addition & 1 deletion server/core/lib/activitypub/playlists/create-update.ts
Expand Up @@ -145,7 +145,7 @@ async function buildElementsDBAttributes (elementUrls: string[], playlist: MVide
try {
const { elementObject } = await fetchRemotePlaylistElement(elementUrl)

const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video' })
const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video-and-blacklist' })

elementsToCreate.push(playlistElementObjectToDBAttributes(elementObject, playlist, video))
} catch (err) {
Expand Down
7 changes: 6 additions & 1 deletion server/core/lib/activitypub/process/process-create.ts
Expand Up @@ -24,7 +24,7 @@ import { createOrUpdateLocalVideoViewer } from '../local-video-viewer.js'
import { createOrUpdateVideoPlaylist } from '../playlists/index.js'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js'
import { resolveThread } from '../video-comments.js'
import { getOrCreateAPVideo } from '../videos/index.js'
import { canVideoBeFederated, getOrCreateAPVideo } from '../videos/index.js'

async function processCreateActivity (options: APProcessorOptions<ActivityCreate<ActivityCreateObject>>) {
const { activity, byActor } = options
Expand Down Expand Up @@ -87,6 +87,11 @@ async function processCreateCacheFile (

const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object })

if (video.isOwned() && !canVideoBeFederated(video)) {
logger.warn(`Do not process create cache file ${cacheFile.object} on a video that cannot be federated`)
return
}

await sequelizeTypescript.transaction(async t => {
return createOrUpdateCacheFile(cacheFile, video, byActor, t)
})
Expand Down
14 changes: 10 additions & 4 deletions server/core/lib/activitypub/process/process-dislike.ts
@@ -1,11 +1,12 @@
import { VideoModel } from '@server/models/video/video.js'
import { ActivityDislike } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { VideoModel } from '@server/models/video/video.js'
import { retryTransactionWrapper } from '../../../helpers/database-utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
import { APProcessorOptions } from '../../../types/activitypub-processor.model.js'
import { MActorSignature } from '../../../types/models/index.js'
import { federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'
import { canVideoBeFederated, federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'

async function processDislikeActivity (options: APProcessorOptions<ActivityDislike>) {
const { activity, byActor } = options
Expand All @@ -21,14 +22,19 @@ export {
// ---------------------------------------------------------------------------

async function processDislike (activity: ActivityDislike, byActor: MActorSignature) {
const dislikeObject = activity.object
const videoUrl = activity.object
const byAccount = byActor.Account

if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)

const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: dislikeObject, fetchType: 'only-video' })
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
if (!onlyVideo?.isOwned()) return

if (!canVideoBeFederated(onlyVideo)) {
logger.warn(`Do not process dislike on video ${videoUrl} that cannot be federated`)
return
}

return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadFull(onlyVideo.id, t)

Expand Down
10 changes: 8 additions & 2 deletions server/core/lib/activitypub/process/process-like.ts
@@ -1,12 +1,13 @@
import { ActivityLike } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { VideoModel } from '@server/models/video/video.js'
import { retryTransactionWrapper } from '../../../helpers/database-utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { getAPId } from '../../../lib/activitypub/activity.js'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
import { APProcessorOptions } from '../../../types/activitypub-processor.model.js'
import { MActorSignature } from '../../../types/models/index.js'
import { federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'
import { canVideoBeFederated, federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'

async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
const { activity, byActor } = options
Expand All @@ -28,9 +29,14 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
const byAccount = byActor.Account
if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)

const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video' })
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
if (!onlyVideo?.isOwned()) return

if (!canVideoBeFederated(onlyVideo)) {
logger.warn(`Do not process like on video ${videoUrl} that cannot be federated`)
return
}

return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadFull(onlyVideo.id, t)

Expand Down
7 changes: 6 additions & 1 deletion server/core/lib/activitypub/process/process-update.ts
Expand Up @@ -20,7 +20,7 @@ import { APActorUpdater } from '../actors/updater.js'
import { createOrUpdateCacheFile } from '../cache-file.js'
import { createOrUpdateVideoPlaylist } from '../playlists/index.js'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js'
import { APVideoUpdater, getOrCreateAPVideo } from '../videos/index.js'
import { APVideoUpdater, canVideoBeFederated, getOrCreateAPVideo } from '../videos/index.js'

async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) {
const { activity, byActor } = options
Expand Down Expand Up @@ -93,6 +93,11 @@ async function processUpdateCacheFile (

const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })

if (video.isOwned() && !canVideoBeFederated(video)) {
logger.warn(`Do not process update cache file on video ${activity.object} that cannot be federated`)
return
}

await sequelizeTypescript.transaction(async t => {
await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)
})
Expand Down
2 changes: 1 addition & 1 deletion server/core/lib/activitypub/process/process-view.ts
Expand Up @@ -24,7 +24,7 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu

const { video } = await getOrCreateAPVideo({
videoObject,
fetchType: 'only-video',
fetchType: 'only-video-and-blacklist',
allowRefresh: false
})

Expand Down

0 comments on commit afb2827

Please sign in to comment.