Add audit logs in various modules

- Videos
- Videos comments
- Users
- Videos channels
- Videos abuses
- Custom config
pull/901/head
Aurélien Bertron 2018-07-31 14:04:26 +02:00 committed by Chocobozzz
parent 5939081838
commit 80e36cd9fa
9 changed files with 249 additions and 20 deletions

View File

@ -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()
}

View File

@ -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)

View File

@ -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)
})

View File

@ -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()
}

View File

@ -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()

View File

@ -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()

View File

@ -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
}

View File

@ -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 = {

View File

@ -454,6 +454,10 @@ export class ActorModel extends Model<ActorModel> {
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
}