audit-logger.ts 6.5 KB

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