From 80e36cd9facb56b330be3e4f1c5ba253cc78c308 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aur=C3=A9lien=20Bertron?= Date: Tue, 31 Jul 2018 14:04:26 +0200 Subject: [PATCH] Add audit logs in various modules - Videos - Videos comments - Users - Videos channels - Videos abuses - Custom config --- server/controllers/api/config.ts | 16 +++ server/controllers/api/users.ts | 49 ++++++-- server/controllers/api/video-channel.ts | 29 ++++- server/controllers/api/videos/abuse.ts | 8 +- server/controllers/api/videos/comment.ts | 10 ++ server/controllers/api/videos/index.ts | 14 ++- server/helpers/audit-logger.ts | 138 ++++++++++++++++++++++- server/lib/user.ts | 1 + server/models/activitypub/actor.ts | 4 + 9 files changed, 249 insertions(+), 20 deletions(-) diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 9c1b2818c..411b13539 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -9,10 +9,13 @@ import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers' import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' import { customConfigUpdateValidator } from '../../middlewares/validators/config' import { ClientHtml } from '../../lib/client-html' +import { CustomConfigAuditView, auditLoggerFactory } from '../../helpers/audit-logger' const packageJSON = require('../../../../package.json') const configRouter = express.Router() +const auditLogger = auditLoggerFactory('config') + configRouter.get('/about', getAbout) configRouter.get('/', asyncMiddleware(getConfig) @@ -119,6 +122,11 @@ async function getCustomConfig (req: express.Request, res: express.Response, nex async function deleteCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { await unlinkPromise(CONFIG.CUSTOM_FILE) + auditLogger.delete( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new CustomConfigAuditView(customConfig()) + ) + reloadConfig() ClientHtml.invalidCache() @@ -129,6 +137,7 @@ async function deleteCustomConfig (req: express.Request, res: express.Response, async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { const toUpdate: CustomConfig = req.body + const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig()) // Force number conversion toUpdate.cache.previews.size = parseInt('' + toUpdate.cache.previews.size, 10) @@ -150,6 +159,13 @@ async function updateCustomConfig (req: express.Request, res: express.Response, ClientHtml.invalidCache() const data = customConfig() + + auditLogger.update( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new CustomConfigAuditView(data), + oldCustomConfigAuditKeys + ) + return res.json(data).end() } diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index c80f27a23..dbe736bff 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -39,6 +39,9 @@ import { createReqFiles } from '../../helpers/express-utils' import { UserVideoQuota } from '../../../shared/models/users/user-video-quota.model' import { updateAvatarValidator } from '../../middlewares/validators/avatar' import { updateActorAvatarFile } from '../../lib/avatar' +import { auditLoggerFactory, UserAuditView } from '../../helpers/audit-logger' + +const auditLogger = auditLoggerFactory('users') const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) const loginRateLimiter = new RateLimit({ @@ -189,6 +192,7 @@ async function createUser (req: express.Request, res: express.Response) { const { user, account } = await createUserAccountAndChannel(userToCreate) + auditLogger.create(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new UserAuditView(user.toFormattedJSON())) logger.info('User %s with its channel and account created.', body.username) return res.json({ @@ -205,7 +209,7 @@ async function createUser (req: express.Request, res: express.Response) { async function registerUser (req: express.Request, res: express.Response) { const body: UserCreate = req.body - const user = new UserModel({ + const userToCreate = new UserModel({ username: body.username, password: body.password, email: body.email, @@ -215,8 +219,9 @@ async function registerUser (req: express.Request, res: express.Response) { videoQuota: CONFIG.USER.VIDEO_QUOTA }) - await createUserAccountAndChannel(user) + const { user } = await createUserAccountAndChannel(userToCreate) + auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) logger.info('User %s with its channel and account registered.', body.username) return res.type('json').status(204).end() @@ -269,6 +274,8 @@ async function removeUser (req: express.Request, res: express.Response, next: ex await user.destroy() + auditLogger.delete(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new UserAuditView(user.toFormattedJSON())) + return res.sendStatus(204) } @@ -276,6 +283,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr const body: UserUpdateMe = req.body const user: UserModel = res.locals.oauth.token.user + const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) if (body.password !== undefined) user.password = body.password if (body.email !== undefined) user.email = body.email @@ -290,6 +298,12 @@ async function updateMe (req: express.Request, res: express.Response, next: expr await user.Account.save({ transaction: t }) await sendUpdateActor(user.Account, t) + + auditLogger.update( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new UserAuditView(user.toFormattedJSON()), + oldUserAuditView + ) }) return res.sendStatus(204) @@ -297,10 +311,18 @@ async function updateMe (req: express.Request, res: express.Response, next: expr async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] - const account = res.locals.oauth.token.user.Account + const user: UserModel = res.locals.oauth.token.user + const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) + const account = user.Account const avatar = await updateActorAvatarFile(avatarPhysicalFile, account.Actor, account) + auditLogger.update( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new UserAuditView(user.toFormattedJSON()), + oldUserAuditView + ) + return res .json({ avatar: avatar.toFormattedJSON() @@ -310,20 +332,27 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { const body: UserUpdate = req.body - const user = res.locals.user as UserModel - const roleChanged = body.role !== undefined && body.role !== user.role + const userToUpdate = res.locals.user as UserModel + const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) + const roleChanged = body.role !== undefined && body.role !== userToUpdate.role - if (body.email !== undefined) user.email = body.email - if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota - if (body.role !== undefined) user.role = body.role + if (body.email !== undefined) userToUpdate.email = body.email + if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota + if (body.role !== undefined) userToUpdate.role = body.role - await user.save() + const user = await userToUpdate.save() // Destroy user token to refresh rights if (roleChanged) { - await OAuthTokenModel.deleteUserToken(user.id) + await OAuthTokenModel.deleteUserToken(userToUpdate.id) } + auditLogger.update( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new UserAuditView(user.toFormattedJSON()), + oldUserAuditView + ) + // Don't need to send this update to followers, these attributes are not propagated return res.sendStatus(204) diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 0488ba8f5..3a444547b 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts @@ -27,7 +27,9 @@ import { logger } from '../../helpers/logger' import { VideoModel } from '../../models/video/video' import { updateAvatarValidator } from '../../middlewares/validators/avatar' import { updateActorAvatarFile } from '../../lib/avatar' +import { auditLoggerFactory, VideoChannelAuditView } from '../../helpers/audit-logger' +const auditLogger = auditLoggerFactory('channels') const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) const videoChannelRouter = express.Router() @@ -99,10 +101,17 @@ async function listVideoChannels (req: express.Request, res: express.Response, n async function updateVideoChannelAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] - const videoChannel = res.locals.videoChannel + const videoChannel = res.locals.videoChannel as VideoChannelModel + const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel.Actor, videoChannel) + auditLogger.update( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new VideoChannelAuditView(videoChannel.toFormattedJSON()), + oldVideoChannelAuditKeys + ) + return res .json({ avatar: avatar.toFormattedJSON() @@ -121,6 +130,10 @@ async function addVideoChannel (req: express.Request, res: express.Response) { setAsyncActorKeys(videoChannelCreated.Actor) .catch(err => logger.error('Cannot set async actor keys for account %s.', videoChannelCreated.Actor.uuid, { err })) + auditLogger.create( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new VideoChannelAuditView(videoChannelCreated.toFormattedJSON()) + ) logger.info('Video channel with uuid %s created.', videoChannelCreated.Actor.uuid) return res.json({ @@ -134,6 +147,7 @@ async function addVideoChannel (req: express.Request, res: express.Response) { async function updateVideoChannel (req: express.Request, res: express.Response) { const videoChannelInstance = res.locals.videoChannel as VideoChannelModel const videoChannelFieldsSave = videoChannelInstance.toJSON() + const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()) const videoChannelInfoToUpdate = req.body as VideoChannelUpdate try { @@ -148,9 +162,14 @@ async function updateVideoChannel (req: express.Request, res: express.Response) const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) await sendUpdateActor(videoChannelInstanceUpdated, t) - }) - logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) + auditLogger.update( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()), + oldVideoChannelAuditKeys + ) + logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) + }) } catch (err) { logger.debug('Cannot update the video channel.', { err }) @@ -171,6 +190,10 @@ async function removeVideoChannel (req: express.Request, res: express.Response) await sequelizeTypescript.transaction(async t => { await videoChannelInstance.destroy({ transaction: t }) + auditLogger.delete( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()) + ) logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) }) diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index 3413ae894..7782fc639 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts @@ -18,7 +18,9 @@ import { import { AccountModel } from '../../../models/account/account' import { VideoModel } from '../../../models/video/video' import { VideoAbuseModel } from '../../../models/video/video-abuse' +import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' +const auditLogger = auditLoggerFactory('abuse') const abuseVideoRouter = express.Router() abuseVideoRouter.get('/abuse', @@ -64,14 +66,16 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) { await sequelizeTypescript.transaction(async t => { const videoAbuseInstance = await VideoAbuseModel.create(abuseToCreate, { transaction: t }) videoAbuseInstance.Video = videoInstance + videoAbuseInstance.Account = reporterAccount // We send the video abuse to the origin server if (videoInstance.isOwned() === false) { await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t) } - }) - logger.info('Abuse report for video %s created.', videoInstance.name) + auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) + logger.info('Abuse report for video %s created.', videoInstance.name) + }) return res.type('json').status(204).end() } diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index bbeb0d557..e35247829 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts @@ -23,7 +23,9 @@ import { } from '../../../middlewares/validators/video-comments' import { VideoModel } from '../../../models/video/video' import { VideoCommentModel } from '../../../models/video/video-comment' +import { auditLoggerFactory, CommentAuditView } from '../../../helpers/audit-logger' +const auditLogger = auditLoggerFactory('comments') const videoCommentRouter = express.Router() videoCommentRouter.get('/:videoId/comment-threads', @@ -107,6 +109,8 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons }, t) }) + auditLogger.create(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new CommentAuditView(comment.toFormattedJSON())) + return res.json({ comment: comment.toFormattedJSON() }).end() @@ -124,6 +128,8 @@ async function addVideoCommentReply (req: express.Request, res: express.Response }, t) }) + auditLogger.create(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new CommentAuditView(comment.toFormattedJSON())) + return res.json({ comment: comment.toFormattedJSON() }).end() @@ -136,6 +142,10 @@ async function removeVideoComment (req: express.Request, res: express.Response) await videoCommentInstance.destroy({ transaction: t }) }) + auditLogger.delete( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new CommentAuditView(videoCommentInstance.toFormattedJSON()) + ) logger.info('Video comment %d deleted.', videoCommentInstance.id) return res.type('json').status(204).end() diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 101183eab..e396ee6be 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -5,6 +5,7 @@ import { renamePromise } from '../../../helpers/core-utils' import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' import { processImage } from '../../../helpers/image-utils' import { logger } from '../../../helpers/logger' +import { auditLoggerFactory, VideoAuditView } from '../../../helpers/audit-logger' import { getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' import { CONFIG, @@ -54,6 +55,7 @@ import { createReqFiles, buildNSFWFilter } from '../../../helpers/express-utils' import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' import { videoCaptionsRouter } from './captions' +const auditLogger = auditLoggerFactory('videos') const videosRouter = express.Router() const reqVideoFileAdd = createReqFiles( @@ -247,6 +249,7 @@ async function addVideo (req: express.Request, res: express.Response) { await federateVideoIfNeeded(video, true, t) + auditLogger.create(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) return videoCreated @@ -273,6 +276,7 @@ async function addVideo (req: express.Request, res: express.Response) { async function updateVideo (req: express.Request, res: express.Response) { const videoInstance: VideoModel = res.locals.video const videoFieldsSave = videoInstance.toJSON() + const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) const videoInfoToUpdate: VideoUpdate = req.body const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE @@ -344,9 +348,14 @@ async function updateVideo (req: express.Request, res: express.Response) { const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) - }) - logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) + auditLogger.update( + res.locals.oauth.token.User.Account.Actor.getIdentifier(), + new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), + oldVideoAuditView + ) + logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) + }) } catch (err) { // Force fields we want to update // If the transaction is retried, sequelize will think the object has not changed @@ -423,6 +432,7 @@ async function removeVideo (req: express.Request, res: express.Response) { await videoInstance.destroy({ transaction: t }) }) + auditLogger.delete(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new VideoAuditView(videoInstance.toFormattedDetailsJSON())) logger.info('Video with name %s and uuid %s deleted.', videoInstance.name, videoInstance.uuid) return res.type('json').status(204).end() diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts index 4b237316f..f6eea7d90 100644 --- a/server/helpers/audit-logger.ts +++ b/server/helpers/audit-logger.ts @@ -5,7 +5,9 @@ import * as flatten from 'flat' import * as winston from 'winston' import { CONFIG } from '../initializers' import { jsonLoggerFormat, labelFormatter } from './logger' -import { VideoDetails } from '../../shared' +import { VideoDetails, User, VideoChannel, VideoAbuse } from '../../shared' +import { VideoComment } from '../../shared/models/videos/video-comment.model' +import { CustomConfig } from '../../shared/models/server/custom-config.model' enum AUDIT_TYPE { CREATE = 'create', @@ -111,13 +113,143 @@ const videoKeysToKeep = [ 'support', 'commentsEnabled' ] -class VideoAuditView extends AuditEntity { +class VideoAuditView extends EntityAuditView { constructor (private video: VideoDetails) { super(videoKeysToKeep, 'video', video) } } +const commentKeysToKeep = [ + 'id', + 'text', + 'threadId', + 'inReplyToCommentId', + 'videoId', + 'createdAt', + 'updatedAt', + 'totalReplies', + 'account-id', + 'account-uuid', + 'account-name' +] +class CommentAuditView extends EntityAuditView { + constructor (private comment: VideoComment) { + super(commentKeysToKeep, 'comment', comment) + } +} + +const userKeysToKeep = [ + 'id', + 'username', + 'email', + 'nsfwPolicy', + 'autoPlayVideo', + 'role', + 'videoQuota', + 'createdAt', + 'account-id', + 'account-uuid', + 'account-name', + 'account-followingCount', + 'account-followersCount', + 'account-createdAt', + 'account-updatedAt', + 'account-avatar-path', + 'account-avatar-createdAt', + 'account-avatar-updatedAt', + 'account-displayName', + 'account-description', + 'videoChannels' +] +class UserAuditView extends EntityAuditView { + constructor (private user: User) { + super(userKeysToKeep, 'user', user) + } +} + +const channelKeysToKeep = [ + 'id', + 'uuid', + 'name', + 'followingCount', + 'followersCount', + 'createdAt', + 'updatedAt', + 'avatar-path', + 'avatar-createdAt', + 'avatar-updatedAt', + 'displayName', + 'description', + 'support', + 'isLocal', + 'ownerAccount-id', + 'ownerAccount-uuid', + 'ownerAccount-name', + 'ownerAccount-displayedName' +] +class VideoChannelAuditView extends EntityAuditView { + constructor (private channel: VideoChannel) { + super(channelKeysToKeep, 'channel', channel) + } +} + +const videoAbuseKeysToKeep = [ + 'id', + 'reason', + 'reporterAccount', + 'video-id', + 'video-name', + 'video-uuid', + 'createdAt' +] +class VideoAbuseAuditView extends EntityAuditView { + constructor (private videoAbuse: VideoAbuse) { + super(videoAbuseKeysToKeep, 'abuse', videoAbuse) + } +} + +const customConfigKeysToKeep = [ + 'instance-name', + 'instance-shortDescription', + 'instance-description', + 'instance-terms', + 'instance-defaultClientRoute', + 'instance-defaultNSFWPolicy', + 'instance-customizations-javascript', + 'instance-customizations-css', + 'services-twitter-username', + 'services-twitter-whitelisted', + 'cache-previews-size', + 'cache-captions-size', + 'signup-enabled', + 'signup-limit', + 'admin-email', + 'user-videoQuota', + 'transcoding-enabled', + 'transcoding-threads', + 'transcoding-resolutions' +] +class CustomConfigAuditView extends EntityAuditView { + constructor (customConfig: CustomConfig) { + const infos: any = customConfig + const resolutionsDict = infos.transcoding.resolutions + const resolutionsArray = [] + Object.entries(resolutionsDict).forEach(([resolution, isEnabled]) => { + if (isEnabled) { + resolutionsArray.push(resolution) + } + }) + infos.transcoding.resolutions = resolutionsArray + super(customConfigKeysToKeep, 'config', infos) + } +} + export { auditLoggerFactory, - VideoAuditView + VideoChannelAuditView, + CommentAuditView, + UserAuditView, + VideoAuditView, + VideoAbuseAuditView, + CustomConfigAuditView } diff --git a/server/lib/user.ts b/server/lib/user.ts index ac5f55260..e7a45f5aa 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -17,6 +17,7 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse const userCreated = await userToCreate.save(userOptions) const accountCreated = await createLocalAccountWithoutKeys(userToCreate.username, userToCreate.id, null, t) + userCreated.Account = accountCreated const videoChannelDisplayName = `Default ${userCreated.username} channel` const videoChannelInfo = { diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 267032e2a..aeb69e7b4 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -454,6 +454,10 @@ export class ActorModel extends Model { return 'acct:' + this.preferredUsername + '@' + this.getHost() } + getIdentifier () { + return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername + } + getHost () { return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST } -- 2.41.0