diff --git a/@xen-orchestra/audit-core/package.json b/@xen-orchestra/audit-core/package.json index d921ee04e..0de5d2005 100644 --- a/@xen-orchestra/audit-core/package.json +++ b/@xen-orchestra/audit-core/package.json @@ -30,6 +30,7 @@ "rimraf": "^3.0.0" }, "dependencies": { + "@xen-orchestra/log": "^0.2.0", "core-js": "^3.6.4", "golike-defer": "^0.4.1", "lodash": "^4.17.15", diff --git a/@xen-orchestra/audit-core/src/index.js b/@xen-orchestra/audit-core/src/index.js index 326e4fdbf..d81ede30a 100644 --- a/@xen-orchestra/audit-core/src/index.js +++ b/@xen-orchestra/audit-core/src/index.js @@ -2,9 +2,12 @@ import 'core-js/features/symbol/async-iterator' import assert from 'assert' +import createLogger from '@xen-orchestra/log' import defer from 'golike-defer' import hash from 'object-hash' +const log = createLogger('xo:audit-core') + export class Storage { constructor() { this._lock = Promise.resolve() @@ -65,8 +68,17 @@ export class AuditCore { @defer async add($defer, subject, event, data) { const time = Date.now() + $defer(await this._storage.acquireLock()) + return this._addUnsafe({ + data, + event, + subject, + time, + }) + } + + async _addUnsafe({ data, event, subject, time }) { const storage = this._storage - $defer(await storage.acquireLock()) // delete "undefined" properties and normalize data with JSON.stringify const record = JSON.parse( @@ -139,4 +151,45 @@ export class AuditCore { await this._storage.del(id) } } + + @defer + async deleteRangeAndRewrite($defer, newest, oldest) { + assert.notStrictEqual(newest, undefined) + assert.notStrictEqual(oldest, undefined) + + const storage = this._storage + $defer(await storage.acquireLock()) + + assert.notStrictEqual(await storage.get(newest), undefined) + const oldestRecord = await storage.get(oldest) + assert.notStrictEqual(oldestRecord, undefined) + + const lastId = await storage.getLastId() + const recentRecords = [] + for await (const record of this.getFrom(lastId)) { + if (record.id === newest) { + break + } + + recentRecords.push(record) + } + + for await (const record of this.getFrom(newest)) { + await storage.del(record.id) + if (record.id === oldest) { + break + } + } + + await storage.setLastId(oldestRecord.previousId) + + for (const record of recentRecords) { + try { + await this._addUnsafe(record) + await storage.del(record.id) + } catch (error) { + log.error(error) + } + } + } } diff --git a/@xen-orchestra/audit-core/src/specification.ts b/@xen-orchestra/audit-core/src/specification.ts index daa27172c..9e2e725ea 100644 --- a/@xen-orchestra/audit-core/src/specification.ts +++ b/@xen-orchestra/audit-core/src/specification.ts @@ -17,9 +17,10 @@ interface Record { } export class AuditCore { - constructor(storage: Storage) {} - public add(subject: any, event: string, data: any): Promise {} - public checkIntegrity(oldest: string, newest: string): Promise {} - public getFrom(newest?: string): AsyncIterator {} - public deleteFrom(newest: string): Promise {} + constructor(storage: Storage) { } + public add(subject: any, event: string, data: any): Promise { } + public checkIntegrity(oldest: string, newest: string): Promise { } + public getFrom(newest?: string): AsyncIterator { } + public deleteFrom(newest: string): Promise { } + public deleteRangeAndRewrite(newest: string, oldest: string): Promise { } } diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 154c5e87a..217725ab7 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -27,3 +27,6 @@ > - major: if the change breaks compatibility > > In case of conflict, the highest (lowest in previous list) `$version` wins. + +- @xen-orchestra/audit-core minor +- xo-server-audit minor diff --git a/packages/xo-server-audit/src/index.js b/packages/xo-server-audit/src/index.js index eecd5e191..fa71af91d 100644 --- a/packages/xo-server-audit/src/index.js +++ b/packages/xo-server-audit/src/index.js @@ -20,6 +20,7 @@ const DEFAULT_BLOCKED_LIST = { 'acl.getCurrentPermissions': true, 'audit.checkIntegrity': true, 'audit.clean': true, + 'audit.deleteRange': true, 'audit.generateFingerprint': true, 'audit.getRecords': true, 'backup.list': true, @@ -238,11 +239,21 @@ class AuditXoPlugin { clean.permission = 'admin' clean.description = 'Clean audit database' + const deleteRange = this._deleteRangeAndRewrite.bind(this) + deleteRange.description = + 'Delete a range of records and rewrite the records chain' + deleteRange.permission = 'admin' + deleteRange.params = { + newest: { type: 'string' }, + oldest: { type: 'string', optional: true }, + } + cleaners.push( this._xo.addApiMethods({ audit: { checkIntegrity, clean, + deleteRange, exportRecords, generateFingerprint, getRecords, @@ -428,6 +439,13 @@ class AuditXoPlugin { throw error } } + + async _deleteRangeAndRewrite({ newest, oldest = newest }) { + await this._auditCore.deleteRangeAndRewrite(newest, oldest) + if (this._uploadLastHashJob !== undefined) { + await this._uploadLastHash() + } + } } AuditXoPlugin.prototype._getRecordsStream = asyncIteratorToStream(