diff options
author | Chocobozzz <me@florianbigard.com> | 2019-12-11 14:14:01 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-12-11 14:14:01 +0100 |
commit | 566c125d6eee3bd907404523d94e1e0b5e403a46 (patch) | |
tree | c477cdd2ba745015d80052968c37927b1bca1254 /server | |
parent | 92e0f42e8ce5f1ab5e4023900b8194627231a11b (diff) | |
download | PeerTube-566c125d6eee3bd907404523d94e1e0b5e403a46.tar.gz PeerTube-566c125d6eee3bd907404523d94e1e0b5e403a46.tar.zst PeerTube-566c125d6eee3bd907404523d94e1e0b5e403a46.zip |
Serve audit logs to client
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/server/logs.ts | 57 | ||||
-rw-r--r-- | server/helpers/audit-logger.ts | 3 | ||||
-rw-r--r-- | server/helpers/logger.ts | 3 | ||||
-rw-r--r-- | server/initializers/constants.ts | 4 | ||||
-rw-r--r-- | server/middlewares/validators/logs.ts | 19 | ||||
-rw-r--r-- | server/tests/api/server/logs.ts | 145 |
6 files changed, 173 insertions, 58 deletions
diff --git a/server/controllers/api/server/logs.ts b/server/controllers/api/server/logs.ts index e9d1f2efd..a0ca21cd5 100644 --- a/server/controllers/api/server/logs.ts +++ b/server/controllers/api/server/logs.ts | |||
@@ -3,11 +3,12 @@ import { UserRight } from '../../../../shared/models/users' | |||
3 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' | 3 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' |
4 | import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs' | 4 | import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs' |
5 | import { readdir, readFile } from 'fs-extra' | 5 | import { readdir, readFile } from 'fs-extra' |
6 | import { MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants' | 6 | import { AUDIT_LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS, LOG_FILENAME } from '../../../initializers/constants' |
7 | import { join } from 'path' | 7 | import { join } from 'path' |
8 | import { getLogsValidator } from '../../../middlewares/validators/logs' | 8 | import { getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs' |
9 | import { LogLevel } from '../../../../shared/models/server/log-level.type' | 9 | import { LogLevel } from '../../../../shared/models/server/log-level.type' |
10 | import { CONFIG } from '../../../initializers/config' | 10 | import { CONFIG } from '../../../initializers/config' |
11 | import { logger } from '@server/helpers/logger' | ||
11 | 12 | ||
12 | const logsRouter = express.Router() | 13 | const logsRouter = express.Router() |
13 | 14 | ||
@@ -18,6 +19,13 @@ logsRouter.get('/logs', | |||
18 | asyncMiddleware(getLogs) | 19 | asyncMiddleware(getLogs) |
19 | ) | 20 | ) |
20 | 21 | ||
22 | logsRouter.get('/audit-logs', | ||
23 | authenticate, | ||
24 | ensureUserHasRight(UserRight.MANAGE_LOGS), | ||
25 | getAuditLogsValidator, | ||
26 | asyncMiddleware(getAuditLogs) | ||
27 | ) | ||
28 | |||
21 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
22 | 30 | ||
23 | export { | 31 | export { |
@@ -26,18 +34,50 @@ export { | |||
26 | 34 | ||
27 | // --------------------------------------------------------------------------- | 35 | // --------------------------------------------------------------------------- |
28 | 36 | ||
37 | const auditLogNameFilter = generateLogNameFilter(AUDIT_LOG_FILENAME) | ||
38 | async function getAuditLogs (req: express.Request, res: express.Response) { | ||
39 | const output = await generateOutput({ | ||
40 | startDateQuery: req.query.startDate, | ||
41 | endDateQuery: req.query.endDate, | ||
42 | level: 'audit', | ||
43 | nameFilter: auditLogNameFilter | ||
44 | }) | ||
45 | |||
46 | return res.json(output).end() | ||
47 | } | ||
48 | |||
49 | const logNameFilter = generateLogNameFilter(LOG_FILENAME) | ||
29 | async function getLogs (req: express.Request, res: express.Response) { | 50 | async function getLogs (req: express.Request, res: express.Response) { |
51 | const output = await generateOutput({ | ||
52 | startDateQuery: req.query.startDate, | ||
53 | endDateQuery: req.query.endDate, | ||
54 | level: req.query.level || 'info', | ||
55 | nameFilter: logNameFilter | ||
56 | }) | ||
57 | |||
58 | return res.json(output).end() | ||
59 | } | ||
60 | |||
61 | async function generateOutput (options: { | ||
62 | startDateQuery: string, | ||
63 | endDateQuery?: string, | ||
64 | level: LogLevel, | ||
65 | nameFilter: RegExp | ||
66 | }) { | ||
67 | const { startDateQuery, level, nameFilter } = options | ||
68 | |||
30 | const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR) | 69 | const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR) |
31 | const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR) | 70 | const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR) |
32 | let currentSize = 0 | 71 | let currentSize = 0 |
33 | 72 | ||
34 | const startDate = new Date(req.query.startDate) | 73 | const startDate = new Date(startDateQuery) |
35 | const endDate = req.query.endDate ? new Date(req.query.endDate) : new Date() | 74 | const endDate = options.endDateQuery ? new Date(options.endDateQuery) : new Date() |
36 | const level: LogLevel = req.query.level || 'info' | ||
37 | 75 | ||
38 | let output: string[] = [] | 76 | let output: string[] = [] |
39 | 77 | ||
40 | for (const meta of sortedLogFiles) { | 78 | for (const meta of sortedLogFiles) { |
79 | if (nameFilter.exec(meta.file) === null) continue | ||
80 | |||
41 | const path = join(CONFIG.STORAGE.LOG_DIR, meta.file) | 81 | const path = join(CONFIG.STORAGE.LOG_DIR, meta.file) |
42 | 82 | ||
43 | const result = await getOutputFromFile(path, startDate, endDate, level, currentSize) | 83 | const result = await getOutputFromFile(path, startDate, endDate, level, currentSize) |
@@ -49,7 +89,7 @@ async function getLogs (req: express.Request, res: express.Response) { | |||
49 | if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break | 89 | if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break |
50 | } | 90 | } |
51 | 91 | ||
52 | return res.json(output).end() | 92 | return output |
53 | } | 93 | } |
54 | 94 | ||
55 | async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) { | 95 | async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) { |
@@ -58,6 +98,7 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date, | |||
58 | let logTime: number | 98 | let logTime: number |
59 | 99 | ||
60 | const logsLevel: { [ id in LogLevel ]: number } = { | 100 | const logsLevel: { [ id in LogLevel ]: number } = { |
101 | audit: -1, | ||
61 | debug: 0, | 102 | debug: 0, |
62 | info: 1, | 103 | info: 1, |
63 | warn: 2, | 104 | warn: 2, |
@@ -93,3 +134,7 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date, | |||
93 | 134 | ||
94 | return { currentSize, output: output.reverse(), logTime } | 135 | return { currentSize, output: output.reverse(), logTime } |
95 | } | 136 | } |
137 | |||
138 | function generateLogNameFilter (baseName: string) { | ||
139 | return new RegExp('^' + baseName.replace(/\.log$/, '') + '\d*.log$') | ||
140 | } | ||
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts index f536da439..9b258dc3a 100644 --- a/server/helpers/audit-logger.ts +++ b/server/helpers/audit-logger.ts | |||
@@ -9,6 +9,7 @@ import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../.. | |||
9 | import { VideoComment } from '../../shared/models/videos/video-comment.model' | 9 | import { VideoComment } from '../../shared/models/videos/video-comment.model' |
10 | import { CustomConfig } from '../../shared/models/server/custom-config.model' | 10 | import { CustomConfig } from '../../shared/models/server/custom-config.model' |
11 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../initializers/config' |
12 | import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' | ||
12 | 13 | ||
13 | function getAuditIdFromRes (res: express.Response) { | 14 | function getAuditIdFromRes (res: express.Response) { |
14 | return res.locals.oauth.token.User.username | 15 | return res.locals.oauth.token.User.username |
@@ -29,7 +30,7 @@ const auditLogger = winston.createLogger({ | |||
29 | levels: { audit: 0 }, | 30 | levels: { audit: 0 }, |
30 | transports: [ | 31 | transports: [ |
31 | new winston.transports.File({ | 32 | new winston.transports.File({ |
32 | filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube-audit.log'), | 33 | filename: path.join(CONFIG.STORAGE.LOG_DIR, AUDIT_LOG_FILENAME), |
33 | level: 'audit', | 34 | level: 'audit', |
34 | maxsize: 5242880, | 35 | maxsize: 5242880, |
35 | maxFiles: 5, | 36 | maxFiles: 5, |
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts index d21746963..c2ff2bae6 100644 --- a/server/helpers/logger.ts +++ b/server/helpers/logger.ts | |||
@@ -5,6 +5,7 @@ import * as winston from 'winston' | |||
5 | import { FileTransportOptions } from 'winston/lib/winston/transports' | 5 | import { FileTransportOptions } from 'winston/lib/winston/transports' |
6 | import { CONFIG } from '../initializers/config' | 6 | import { CONFIG } from '../initializers/config' |
7 | import { omit } from 'lodash' | 7 | import { omit } from 'lodash' |
8 | import { LOG_FILENAME } from '@server/initializers/constants' | ||
8 | 9 | ||
9 | const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT | 10 | const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT |
10 | 11 | ||
@@ -58,7 +59,7 @@ const labelFormatter = winston.format.label({ | |||
58 | }) | 59 | }) |
59 | 60 | ||
60 | const fileLoggerOptions: FileTransportOptions = { | 61 | const fileLoggerOptions: FileTransportOptions = { |
61 | filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'), | 62 | filename: path.join(CONFIG.STORAGE.LOG_DIR, LOG_FILENAME), |
62 | handleExceptions: true, | 63 | handleExceptions: true, |
63 | format: winston.format.combine( | 64 | format: winston.format.combine( |
64 | winston.format.timestamp(), | 65 | winston.format.timestamp(), |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index af70e7b88..bdabe7f66 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -603,6 +603,8 @@ const FEEDS = { | |||
603 | } | 603 | } |
604 | 604 | ||
605 | const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 | 605 | const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 |
606 | const LOG_FILENAME = 'peertube.log' | ||
607 | const AUDIT_LOG_FILENAME = 'peertube-audit.log' | ||
606 | 608 | ||
607 | // --------------------------------------------------------------------------- | 609 | // --------------------------------------------------------------------------- |
608 | 610 | ||
@@ -684,6 +686,7 @@ export { | |||
684 | BCRYPT_SALT_SIZE, | 686 | BCRYPT_SALT_SIZE, |
685 | TRACKER_RATE_LIMITS, | 687 | TRACKER_RATE_LIMITS, |
686 | FILES_CACHE, | 688 | FILES_CACHE, |
689 | LOG_FILENAME, | ||
687 | CONSTRAINTS_FIELDS, | 690 | CONSTRAINTS_FIELDS, |
688 | EMBED_SIZE, | 691 | EMBED_SIZE, |
689 | REDUNDANCY, | 692 | REDUNDANCY, |
@@ -693,6 +696,7 @@ export { | |||
693 | OAUTH_LIFETIME, | 696 | OAUTH_LIFETIME, |
694 | CUSTOM_HTML_TAG_COMMENTS, | 697 | CUSTOM_HTML_TAG_COMMENTS, |
695 | BROADCAST_CONCURRENCY, | 698 | BROADCAST_CONCURRENCY, |
699 | AUDIT_LOG_FILENAME, | ||
696 | PAGINATION, | 700 | PAGINATION, |
697 | ACTOR_FOLLOW_SCORE, | 701 | ACTOR_FOLLOW_SCORE, |
698 | PREVIEWS_SIZE, | 702 | PREVIEWS_SIZE, |
diff --git a/server/middlewares/validators/logs.ts b/server/middlewares/validators/logs.ts index 07f3f552f..70e4d0d99 100644 --- a/server/middlewares/validators/logs.ts +++ b/server/middlewares/validators/logs.ts | |||
@@ -24,8 +24,25 @@ const getLogsValidator = [ | |||
24 | } | 24 | } |
25 | ] | 25 | ] |
26 | 26 | ||
27 | const getAuditLogsValidator = [ | ||
28 | query('startDate') | ||
29 | .custom(isDateValid).withMessage('Should have a valid start date'), | ||
30 | query('endDate') | ||
31 | .optional() | ||
32 | .custom(isDateValid).withMessage('Should have a valid end date'), | ||
33 | |||
34 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
35 | logger.debug('Checking getAuditLogsValidator parameters.', { parameters: req.query }) | ||
36 | |||
37 | if (areValidationErrors(req, res)) return | ||
38 | |||
39 | return next() | ||
40 | } | ||
41 | ] | ||
42 | |||
27 | // --------------------------------------------------------------------------- | 43 | // --------------------------------------------------------------------------- |
28 | 44 | ||
29 | export { | 45 | export { |
30 | getLogsValidator | 46 | getLogsValidator, |
47 | getAuditLogsValidator | ||
31 | } | 48 | } |
diff --git a/server/tests/api/server/logs.ts b/server/tests/api/server/logs.ts index 68f442199..d3c877408 100644 --- a/server/tests/api/server/logs.ts +++ b/server/tests/api/server/logs.ts | |||
@@ -2,17 +2,10 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { | 5 | import { cleanupTests, flushAndRunServer, ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index' |
6 | flushTests, | ||
7 | killallServers, | ||
8 | flushAndRunServer, | ||
9 | ServerInfo, | ||
10 | setAccessTokensToServers, | ||
11 | cleanupTests | ||
12 | } from '../../../../shared/extra-utils/index' | ||
13 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 6 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
14 | import { uploadVideo } from '../../../../shared/extra-utils/videos/videos' | 7 | import { uploadVideo } from '../../../../shared/extra-utils/videos/videos' |
15 | import { getLogs } from '../../../../shared/extra-utils/logs/logs' | 8 | import { getAuditLogs, getLogs } from '../../../../shared/extra-utils/logs/logs' |
16 | 9 | ||
17 | const expect = chai.expect | 10 | const expect = chai.expect |
18 | 11 | ||
@@ -26,69 +19,123 @@ describe('Test logs', function () { | |||
26 | await setAccessTokensToServers([ server ]) | 19 | await setAccessTokensToServers([ server ]) |
27 | }) | 20 | }) |
28 | 21 | ||
29 | it('Should get logs with a start date', async function () { | 22 | describe('With the standard log file', function () { |
30 | this.timeout(10000) | 23 | it('Should get logs with a start date', async function () { |
24 | this.timeout(10000) | ||
31 | 25 | ||
32 | await uploadVideo(server.url, server.accessToken, { name: 'video 1' }) | 26 | await uploadVideo(server.url, server.accessToken, { name: 'video 1' }) |
33 | await waitJobs([ server ]) | 27 | await waitJobs([ server ]) |
34 | 28 | ||
35 | const now = new Date() | 29 | const now = new Date() |
36 | 30 | ||
37 | await uploadVideo(server.url, server.accessToken, { name: 'video 2' }) | 31 | await uploadVideo(server.url, server.accessToken, { name: 'video 2' }) |
38 | await waitJobs([ server ]) | 32 | await waitJobs([ server ]) |
39 | 33 | ||
40 | const res = await getLogs(server.url, server.accessToken, now) | 34 | const res = await getLogs(server.url, server.accessToken, now) |
41 | const logsString = JSON.stringify(res.body) | 35 | const logsString = JSON.stringify(res.body) |
42 | 36 | ||
43 | expect(logsString.includes('video 1')).to.be.false | 37 | expect(logsString.includes('video 1')).to.be.false |
44 | expect(logsString.includes('video 2')).to.be.true | 38 | expect(logsString.includes('video 2')).to.be.true |
45 | }) | 39 | }) |
40 | |||
41 | it('Should get logs with an end date', async function () { | ||
42 | this.timeout(20000) | ||
43 | |||
44 | await uploadVideo(server.url, server.accessToken, { name: 'video 3' }) | ||
45 | await waitJobs([ server ]) | ||
46 | |||
47 | const now1 = new Date() | ||
48 | |||
49 | await uploadVideo(server.url, server.accessToken, { name: 'video 4' }) | ||
50 | await waitJobs([ server ]) | ||
51 | |||
52 | const now2 = new Date() | ||
53 | |||
54 | await uploadVideo(server.url, server.accessToken, { name: 'video 5' }) | ||
55 | await waitJobs([ server ]) | ||
56 | |||
57 | const res = await getLogs(server.url, server.accessToken, now1, now2) | ||
58 | const logsString = JSON.stringify(res.body) | ||
46 | 59 | ||
47 | it('Should get logs with an end date', async function () { | 60 | expect(logsString.includes('video 3')).to.be.false |
48 | this.timeout(20000) | 61 | expect(logsString.includes('video 4')).to.be.true |
62 | expect(logsString.includes('video 5')).to.be.false | ||
63 | }) | ||
49 | 64 | ||
50 | await uploadVideo(server.url, server.accessToken, { name: 'video 3' }) | 65 | it('Should get filter by level', async function () { |
51 | await waitJobs([ server ]) | 66 | this.timeout(10000) |
52 | 67 | ||
53 | const now1 = new Date() | 68 | const now = new Date() |
54 | 69 | ||
55 | await uploadVideo(server.url, server.accessToken, { name: 'video 4' }) | 70 | await uploadVideo(server.url, server.accessToken, { name: 'video 6' }) |
56 | await waitJobs([ server ]) | 71 | await waitJobs([ server ]) |
57 | 72 | ||
58 | const now2 = new Date() | 73 | { |
74 | const res = await getLogs(server.url, server.accessToken, now, undefined, 'info') | ||
75 | const logsString = JSON.stringify(res.body) | ||
59 | 76 | ||
60 | await uploadVideo(server.url, server.accessToken, { name: 'video 5' }) | 77 | expect(logsString.includes('video 6')).to.be.true |
61 | await waitJobs([ server ]) | 78 | } |
62 | 79 | ||
63 | const res = await getLogs(server.url, server.accessToken, now1, now2) | 80 | { |
64 | const logsString = JSON.stringify(res.body) | 81 | const res = await getLogs(server.url, server.accessToken, now, undefined, 'warn') |
82 | const logsString = JSON.stringify(res.body) | ||
65 | 83 | ||
66 | expect(logsString.includes('video 3')).to.be.false | 84 | expect(logsString.includes('video 6')).to.be.false |
67 | expect(logsString.includes('video 4')).to.be.true | 85 | } |
68 | expect(logsString.includes('video 5')).to.be.false | 86 | }) |
69 | }) | 87 | }) |
70 | 88 | ||
71 | it('Should get filter by level', async function () { | 89 | describe('With the audit log', function () { |
72 | this.timeout(10000) | 90 | it('Should get logs with a start date', async function () { |
91 | this.timeout(10000) | ||
73 | 92 | ||
74 | const now = new Date() | 93 | await uploadVideo(server.url, server.accessToken, { name: 'video 7' }) |
94 | await waitJobs([ server ]) | ||
75 | 95 | ||
76 | await uploadVideo(server.url, server.accessToken, { name: 'video 6' }) | 96 | const now = new Date() |
77 | await waitJobs([ server ]) | ||
78 | 97 | ||
79 | { | 98 | await uploadVideo(server.url, server.accessToken, { name: 'video 8' }) |
80 | const res = await getLogs(server.url, server.accessToken, now, undefined, 'info') | 99 | await waitJobs([ server ]) |
100 | |||
101 | const res = await getAuditLogs(server.url, server.accessToken, now) | ||
81 | const logsString = JSON.stringify(res.body) | 102 | const logsString = JSON.stringify(res.body) |
82 | 103 | ||
83 | expect(logsString.includes('video 6')).to.be.true | 104 | expect(logsString.includes('video 7')).to.be.false |
84 | } | 105 | expect(logsString.includes('video 8')).to.be.true |
106 | |||
107 | expect(res.body).to.have.lengthOf(1) | ||
108 | |||
109 | const item = res.body[0] | ||
110 | |||
111 | const message = JSON.parse(item.message) | ||
112 | expect(message.domain).to.equal('videos') | ||
113 | expect(message.action).to.equal('create') | ||
114 | }) | ||
115 | |||
116 | it('Should get logs with an end date', async function () { | ||
117 | this.timeout(20000) | ||
118 | |||
119 | await uploadVideo(server.url, server.accessToken, { name: 'video 9' }) | ||
120 | await waitJobs([ server ]) | ||
121 | |||
122 | const now1 = new Date() | ||
123 | |||
124 | await uploadVideo(server.url, server.accessToken, { name: 'video 10' }) | ||
125 | await waitJobs([ server ]) | ||
126 | |||
127 | const now2 = new Date() | ||
128 | |||
129 | await uploadVideo(server.url, server.accessToken, { name: 'video 11' }) | ||
130 | await waitJobs([ server ]) | ||
85 | 131 | ||
86 | { | 132 | const res = await getAuditLogs(server.url, server.accessToken, now1, now2) |
87 | const res = await getLogs(server.url, server.accessToken, now, undefined, 'warn') | ||
88 | const logsString = JSON.stringify(res.body) | 133 | const logsString = JSON.stringify(res.body) |
89 | 134 | ||
90 | expect(logsString.includes('video 6')).to.be.false | 135 | expect(logsString.includes('video 9')).to.be.false |
91 | } | 136 | expect(logsString.includes('video 10')).to.be.true |
137 | expect(logsString.includes('video 11')).to.be.false | ||
138 | }) | ||
92 | }) | 139 | }) |
93 | 140 | ||
94 | after(async function () { | 141 | after(async function () { |