From 80e36cd9facb56b330be3e4f1c5ba253cc78c308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertron?= Date: Tue, 31 Jul 2018 14:04:26 +0200 Subject: 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 +++++++-- 6 files changed, 109 insertions(+), 17 deletions(-) (limited to 'server/controllers/api') 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() -- cgit v1.2.3