feat(audit-log): version 1 (#4740)

Fixes #4653 #701
This commit is contained in:
badrAZ
2020-02-26 16:25:35 +01:00
committed by GitHub
parent 59e8b26015
commit 6475b58541
24 changed files with 2006 additions and 7 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)
+10
View File
@@ -0,0 +1,10 @@
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
+50
View File
@@ -0,0 +1,50 @@
# xo-server-audit [![Build Status](https://api.travis-ci.org/vatesfr/xo-server-audit.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
> **TODO**
## Install
Installation of the [npm package](https://npmjs.org/package/xo-server-audit):
```
> npm install --global xo-server-audit
```
## Usage
Like all other xo-server plugins, it can be configured directly via
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
## Development
```
# Install dependencies
> yarn
# Run the tests
> yarn test
# Continuously compile
> yarn dev
# Continuously run the tests
> yarn dev-test
# Build for production
> yarn build
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
AGPL3 © [Vates SAS](http://vates.fr)
+39
View File
@@ -0,0 +1,39 @@
# List of skipped methods
blockedList = [
'acl.get',
'acl.getCurrentPermissions',
'audit.checkIntegrity',
'audit.generateFingerprint',
'audit.getRecords',
'backupNg.getAllJobs',
'backupNg.getAllLogs',
'cloud.getResourceCatalog',
'cloudConfig.getAll',
'group.getAll',
'host.stats',
'ipPool.getAll',
'job.getAll',
'log.get',
'metadataBackup.getAllJobs',
'plugin.get',
'pool.listMissingPatches',
'proxy.getAll',
'remote.getAll',
'remote.getAllInfo',
'resourceSet.getAll',
'role.getAll',
'schedule.getAll',
'server.getAll',
'session.getUser',
'sr.getUnhealthyVdiChainsLength',
'sr.stats',
'system.getMethodsInfo',
'system.getServerTimezone',
'system.getServerVersion',
'user.getAll',
'vm.stats',
'xo.getAllObjects',
'xoa.supportTunnel.getState',
'xosan.checkSrCurrentState',
'xosan.getVolumeInfo',
]
+60
View File
@@ -0,0 +1,60 @@
{
"name": "xo-server-audit",
"version": "0.0.0",
"license": "AGPL-3.0",
"description": "Audit plugin for XO-Server",
"keywords": [
"audit",
"log",
"logs",
"orchestra",
"plugin",
"xen-orchestra",
"xen",
"xo-server"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-audit",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "packages/xo-server-audit",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@isonoe.net"
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"files": [
"dist/"
],
"engines": {
"node": ">=6"
},
"devDependencies": {
"@babel/cli": "^7.7.0",
"@babel/core": "^7.7.2",
"@babel/preset-env": "^7.7.1",
"cross-env": "^6.0.3",
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"dependencies": {
"@xen-orchestra/audit-core": "^0.0.0",
"@xen-orchestra/log": "^0.2.0",
"app-conf": "^0.7.1",
"async-iterator-to-stream": "^1.1.0",
"promise-toolbox": "^0.15.0",
"readable-stream": "^3.5.0",
"xo-common": "^0.3.0"
},
"private": true
}
+222
View File
@@ -0,0 +1,222 @@
import appConf from 'app-conf'
import asyncIteratorToStream from 'async-iterator-to-stream'
import createLogger from '@xen-orchestra/log'
import path from 'path'
import { alteredAuditRecord, missingAuditRecord } from 'xo-common/api-errors'
import { fromCallback } from 'promise-toolbox'
import { pipeline } from 'readable-stream'
import {
AlteredRecordError,
AuditCore,
MissingRecordError,
NULL_ID,
Storage,
} from '@xen-orchestra/audit-core'
const log = createLogger('xo:xo-server-audit')
const LAST_ID = 'lastId'
class Db extends Storage {
constructor(db) {
super()
this._db = db
}
async put(record) {
await this._db.put(record.id, record)
}
async del(id) {
await this._db.del(id)
}
get(id) {
return this._db.get(id).catch(error => {
if (!error.notFound) {
throw error
}
})
}
async setLastId(id) {
await this._db.put(LAST_ID, id)
}
getLastId() {
return this.get(LAST_ID)
}
}
const NAMESPACE = 'audit'
class AuditXoPlugin {
constructor({ xo }) {
this._cleaners = []
this._xo = xo
this._auditCore = undefined
this._blockedList = undefined
this._storage = undefined
}
async load() {
const cleaners = this._cleaners
try {
const storage = (this._storage = new Db(
await this._xo.getStore(NAMESPACE)
))
this._auditCore = new AuditCore(storage)
this._blockedList = (
await appConf.load('xo-server-audit', {
appDir: path.join(__dirname, '..'),
})
).blockedList
cleaners.push(() => {
this._auditCore = undefined
this._blockedList = undefined
this._storage = undefined
})
} catch (error) {
this._auditCore = undefined
this._blockedList = undefined
this._storage = undefined
throw error
}
this._addListener('xo:postCall', this._handleEvent.bind(this, 'apiCall'))
this._addListener('xo:audit', this._handleEvent.bind(this))
const getRecords = this._getRecords.bind(this)
getRecords.description = 'Get records from a passed record ID'
getRecords.permission = 'admin'
getRecords.params = {
id: { type: 'string', optional: true },
ndjson: { type: 'boolean', optional: true },
}
const checkIntegrity = this._checkIntegrity.bind(this)
checkIntegrity.description =
'Check records integrity between oldest and newest'
checkIntegrity.permission = 'admin'
checkIntegrity.params = {
newest: { type: 'string', optional: true },
oldest: { type: 'string', optional: true },
}
const generateFingerprint = this._generateFingerprint.bind(this)
generateFingerprint.description =
'Generate a fingerprint of the chain oldest-newest'
generateFingerprint.permission = 'admin'
generateFingerprint.params = {
newest: { type: 'string', optional: true },
oldest: { type: 'string', optional: true },
}
cleaners.push(
this._xo.addApiMethods({
audit: {
checkIntegrity,
generateFingerprint,
getRecords,
},
})
)
}
unload() {
this._cleaners.forEach(cleaner => cleaner())
this._cleaners.length = 0
}
_addListener(event, listener_) {
const listener = async (...args) => {
try {
await listener_(...args)
} catch (error) {
log.error(error)
}
}
const xo = this._xo
xo.on(event, listener)
this._cleaners.push(() => xo.removeListener(event, listener))
}
_handleEvent(event, { userId, userIp, userName, ...data }) {
if (event !== 'apiCall' || this._blockedList.indexOf(data.method) === -1) {
return this._auditCore.add(
{
userId,
userIp,
userName,
},
event,
data
)
}
}
_handleGetRecords(req, res, id) {
res.set('Content-Type', 'application/json')
return fromCallback(
pipeline,
asyncIteratorToStream(async function*(asyncIterator) {
for await (const record of asyncIterator) {
yield JSON.stringify(record)
yield '\n'
}
})(this._auditCore.getFrom(id)),
res
)
}
async _getRecords({ id, ndjson = false }) {
if (ndjson) {
return this._xo
.registerHttpRequest(this._handleGetRecords.bind(this), id)
.then($getFrom => ({
$getFrom,
}))
}
const records = []
for await (const record of this._auditCore.getFrom(id)) {
records.push(record)
}
return records
}
async _checkIntegrity(props) {
const { oldest = NULL_ID, newest = await this._storage.getLastId() } = props
return this._auditCore.checkIntegrity(oldest, newest).catch(error => {
if (error instanceof MissingRecordError) {
throw missingAuditRecord(error)
}
if (error instanceof AlteredRecordError) {
throw alteredAuditRecord(error)
}
throw error
})
}
async _generateFingerprint(props) {
const { oldest = NULL_ID, newest = await this._storage.getLastId() } = props
try {
return {
fingerprint: `${oldest}|${newest}`,
nValid: await this._checkIntegrity({ oldest, newest }),
}
} catch (error) {
if (missingAuditRecord.is(error) || alteredAuditRecord.is(error)) {
return {
fingerprint: `${error.data.id}|${newest}`,
nValid: error.data.nValid,
error,
}
}
throw error
}
}
}
export default opts => new AuditXoPlugin(opts)