audit-logger.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import { diff } from 'deep-object-diff'
  2. import * as express from 'express'
  3. import * as flatten from 'flat'
  4. import { chain } from 'lodash'
  5. import * as path from 'path'
  6. import * as winston from 'winston'
  7. import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
  8. import { AdminAbuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared'
  9. import { CustomConfig } from '../../shared/models/server/custom-config.model'
  10. import { VideoComment } from '../../shared/models/videos/video-comment.model'
  11. import { CONFIG } from '../initializers/config'
  12. import { jsonLoggerFormat, labelFormatter } from './logger'
  13. function getAuditIdFromRes (res: express.Response) {
  14. return res.locals.oauth.token.User.username
  15. }
  16. enum AUDIT_TYPE {
  17. CREATE = 'create',
  18. UPDATE = 'update',
  19. DELETE = 'delete'
  20. }
  21. const colors = winston.config.npm.colors
  22. colors.audit = winston.config.npm.colors.info
  23. winston.addColors(colors)
  24. const auditLogger = winston.createLogger({
  25. levels: { audit: 0 },
  26. transports: [
  27. new winston.transports.File({
  28. filename: path.join(CONFIG.STORAGE.LOG_DIR, AUDIT_LOG_FILENAME),
  29. level: 'audit',
  30. maxsize: 5242880,
  31. maxFiles: 5,
  32. format: winston.format.combine(
  33. winston.format.timestamp(),
  34. labelFormatter(),
  35. winston.format.splat(),
  36. jsonLoggerFormat
  37. )
  38. })
  39. ],
  40. exitOnError: true
  41. })
  42. function auditLoggerWrapper (domain: string, user: string, action: AUDIT_TYPE, entity: EntityAuditView, oldEntity: EntityAuditView = null) {
  43. let entityInfos: object
  44. if (action === AUDIT_TYPE.UPDATE && oldEntity) {
  45. const oldEntityKeys = oldEntity.toLogKeys()
  46. const diffObject = diff(oldEntityKeys, entity.toLogKeys())
  47. const diffKeys = Object.entries(diffObject).reduce((newKeys, entry) => {
  48. newKeys[`new-${entry[0]}`] = entry[1]
  49. return newKeys
  50. }, {})
  51. entityInfos = { ...oldEntityKeys, ...diffKeys }
  52. } else {
  53. entityInfos = { ...entity.toLogKeys() }
  54. }
  55. auditLogger.log('audit', JSON.stringify({
  56. user,
  57. domain,
  58. action,
  59. ...entityInfos
  60. }))
  61. }
  62. function auditLoggerFactory (domain: string) {
  63. return {
  64. create (user: string, entity: EntityAuditView) {
  65. auditLoggerWrapper(domain, user, AUDIT_TYPE.CREATE, entity)
  66. },
  67. update (user: string, entity: EntityAuditView, oldEntity: EntityAuditView) {
  68. auditLoggerWrapper(domain, user, AUDIT_TYPE.UPDATE, entity, oldEntity)
  69. },
  70. delete (user: string, entity: EntityAuditView) {
  71. auditLoggerWrapper(domain, user, AUDIT_TYPE.DELETE, entity)
  72. }
  73. }
  74. }
  75. abstract class EntityAuditView {
  76. constructor (private readonly keysToKeep: string[], private readonly prefix: string, private readonly entityInfos: object) { }
  77. toLogKeys (): object {
  78. return chain(flatten(this.entityInfos, { delimiter: '-', safe: true }))
  79. .pick(this.keysToKeep)
  80. .mapKeys((value, key) => `${this.prefix}-${key}`)
  81. .value()
  82. }
  83. }
  84. const videoKeysToKeep = [
  85. 'tags',
  86. 'uuid',
  87. 'id',
  88. 'uuid',
  89. 'createdAt',
  90. 'updatedAt',
  91. 'publishedAt',
  92. 'category',
  93. 'licence',
  94. 'language',
  95. 'privacy',
  96. 'description',
  97. 'duration',
  98. 'isLocal',
  99. 'name',
  100. 'thumbnailPath',
  101. 'previewPath',
  102. 'nsfw',
  103. 'waitTranscoding',
  104. 'account-id',
  105. 'account-uuid',
  106. 'account-name',
  107. 'channel-id',
  108. 'channel-uuid',
  109. 'channel-name',
  110. 'support',
  111. 'commentsEnabled',
  112. 'downloadEnabled'
  113. ]
  114. class VideoAuditView extends EntityAuditView {
  115. constructor (private readonly video: VideoDetails) {
  116. super(videoKeysToKeep, 'video', video)
  117. }
  118. }
  119. const videoImportKeysToKeep = [
  120. 'id',
  121. 'targetUrl',
  122. 'video-name'
  123. ]
  124. class VideoImportAuditView extends EntityAuditView {
  125. constructor (private readonly videoImport: VideoImport) {
  126. super(videoImportKeysToKeep, 'video-import', videoImport)
  127. }
  128. }
  129. const commentKeysToKeep = [
  130. 'id',
  131. 'text',
  132. 'threadId',
  133. 'inReplyToCommentId',
  134. 'videoId',
  135. 'createdAt',
  136. 'updatedAt',
  137. 'totalReplies',
  138. 'account-id',
  139. 'account-uuid',
  140. 'account-name'
  141. ]
  142. class CommentAuditView extends EntityAuditView {
  143. constructor (private readonly comment: VideoComment) {
  144. super(commentKeysToKeep, 'comment', comment)
  145. }
  146. }
  147. const userKeysToKeep = [
  148. 'id',
  149. 'username',
  150. 'email',
  151. 'nsfwPolicy',
  152. 'autoPlayVideo',
  153. 'role',
  154. 'videoQuota',
  155. 'createdAt',
  156. 'account-id',
  157. 'account-uuid',
  158. 'account-name',
  159. 'account-followingCount',
  160. 'account-followersCount',
  161. 'account-createdAt',
  162. 'account-updatedAt',
  163. 'account-avatar-path',
  164. 'account-avatar-createdAt',
  165. 'account-avatar-updatedAt',
  166. 'account-displayName',
  167. 'account-description',
  168. 'videoChannels'
  169. ]
  170. class UserAuditView extends EntityAuditView {
  171. constructor (private readonly user: User) {
  172. super(userKeysToKeep, 'user', user)
  173. }
  174. }
  175. const channelKeysToKeep = [
  176. 'id',
  177. 'uuid',
  178. 'name',
  179. 'followingCount',
  180. 'followersCount',
  181. 'createdAt',
  182. 'updatedAt',
  183. 'avatar-path',
  184. 'avatar-createdAt',
  185. 'avatar-updatedAt',
  186. 'displayName',
  187. 'description',
  188. 'support',
  189. 'isLocal',
  190. 'ownerAccount-id',
  191. 'ownerAccount-uuid',
  192. 'ownerAccount-name',
  193. 'ownerAccount-displayedName'
  194. ]
  195. class VideoChannelAuditView extends EntityAuditView {
  196. constructor (private readonly channel: VideoChannel) {
  197. super(channelKeysToKeep, 'channel', channel)
  198. }
  199. }
  200. const abuseKeysToKeep = [
  201. 'id',
  202. 'reason',
  203. 'reporterAccount',
  204. 'createdAt'
  205. ]
  206. class AbuseAuditView extends EntityAuditView {
  207. constructor (private readonly abuse: AdminAbuse) {
  208. super(abuseKeysToKeep, 'abuse', abuse)
  209. }
  210. }
  211. const customConfigKeysToKeep = [
  212. 'instance-name',
  213. 'instance-shortDescription',
  214. 'instance-description',
  215. 'instance-terms',
  216. 'instance-defaultClientRoute',
  217. 'instance-defaultNSFWPolicy',
  218. 'instance-customizations-javascript',
  219. 'instance-customizations-css',
  220. 'services-twitter-username',
  221. 'services-twitter-whitelisted',
  222. 'cache-previews-size',
  223. 'cache-captions-size',
  224. 'signup-enabled',
  225. 'signup-limit',
  226. 'signup-requiresEmailVerification',
  227. 'admin-email',
  228. 'user-videoQuota',
  229. 'transcoding-enabled',
  230. 'transcoding-threads',
  231. 'transcoding-resolutions'
  232. ]
  233. class CustomConfigAuditView extends EntityAuditView {
  234. constructor (customConfig: CustomConfig) {
  235. const infos: any = customConfig
  236. const resolutionsDict = infos.transcoding.resolutions
  237. const resolutionsArray = []
  238. Object.entries(resolutionsDict)
  239. .forEach(([ resolution, isEnabled ]) => {
  240. if (isEnabled) resolutionsArray.push(resolution)
  241. })
  242. Object.assign({}, infos, { transcoding: { resolutions: resolutionsArray } })
  243. super(customConfigKeysToKeep, 'config', infos)
  244. }
  245. }
  246. export {
  247. getAuditIdFromRes,
  248. auditLoggerFactory,
  249. VideoImportAuditView,
  250. VideoChannelAuditView,
  251. CommentAuditView,
  252. UserAuditView,
  253. VideoAuditView,
  254. AbuseAuditView,
  255. CustomConfigAuditView
  256. }