feat(xo-server): server side authorization (#6107)

This commit is contained in:
Florent BEAUCHAMP 2022-03-10 11:45:04 +01:00 committed by GitHub
parent 0b41a2b132
commit 8ce1b4bf71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 119 additions and 0 deletions

View File

@ -199,3 +199,12 @@ export const incorrectState = create(25, ({ actual, expected, object, property }
},
message: 'incorrect state',
}))
export const featureUnauthorized = create(26, ({ featureCode, currentPlan, minPlan }) => ({
data: {
featureCode,
currentPlan,
minPlan,
},
message: 'feature Unauthorized',
}))

View File

@ -0,0 +1,67 @@
import get from 'lodash/get.js'
import { featureUnauthorized } from 'xo-common/api-errors.js'
import assert from 'assert'
const FREE = 1
const STARTER = 2
const ENTREPRISE = 3
const PREMIUM = 4
const COMMUNITY = 5 // compiled from sources
export const PLANS = {
free: FREE,
starter: STARTER,
entreprise: ENTREPRISE,
premium: PREMIUM,
}
const AUTHORIZATIONS = {
BACKUP: {
DELTA: STARTER,
DELTA_REPLICATION: ENTREPRISE,
FULL: STARTER,
METADATA: ENTREPRISE,
WITH_RAM: ENTREPRISE,
},
DOCKER: STARTER, // @todo _doDockerAction in xen-orchestra/packages/xo-server/src/xapi/index.mjs
EXPORT: {
XVA: STARTER, // @todo handleExport in xen-orchestra/packages/xo-server/src/api/vm.mjs
},
}
export default class Authorization {
#app
constructor(app) {
this.#app = app
}
#getMinPlan(featureCode) {
const minPlan = get(AUTHORIZATIONS, featureCode)
assert.notEqual(minPlan, undefined, `${featureCode} is not defined in the AUTHORIZATIONS object`)
return minPlan
}
async #getCurrentPlan() {
if (this.#app.getXoaPlan === undefined) {
// source user => everything is open
return COMMUNITY
}
const plan = await this.#app.getXoaPlan()
assert.notEqual(PLANS[plan], undefined, `plan ${plan} is not defined in the PLANS object`)
return PLANS[plan]
}
async checkFeatureAuthorization(featureCode) {
const minPlan = this.#getMinPlan(featureCode)
const currentPlan = await this.#getCurrentPlan()
if (currentPlan < minPlan) {
throw featureUnauthorized({
featureCode,
currentPlan,
minPlan,
})
}
}
}

View File

@ -151,6 +151,8 @@ export default class BackupNg {
// Make sure we are passing only the VM to run which can be
// different than the VMs in the job itself.
let vmIds = data?.vms ?? extractIdsFromSimplePattern(vmsPattern)
await this.checkAuthorizations({ job, schedule, useSmartBackup: vmIds === undefined })
if (vmIds === undefined) {
const poolPattern = vmsPattern.$pool
@ -370,6 +372,43 @@ export default class BackupNg {
return job
}
async checkAuthorizations({ job, useSmartBackup, schedule }) {
const { _app: app } = this
if (job.type === 'metadataBackup') {
await app.checkFeatureAuthorization('BACKUP.METADATA')
// the other checks does not apply to metadata backups
return
}
if (job.mode === 'full') {
await app.checkFeatureAuthorization('BACKUP.FULL')
}
if (job.mode === 'delta') {
if (unboxIdsFromPattern(job.srs)?.length > 0) {
await app.checkFeatureAuthorization('BACKUP.DELTA_REPLICATION')
} else {
await app.checkFeatureAuthorization('BACKUP.DELTA')
}
}
if (useSmartBackup) {
await app.checkFeatureAuthorization('BACKUP.SMART_BACKUP')
}
// this won't check a per VM settings
const config = app.config.get('backups')
const jobSettings = {
...config.defaultSettings,
...config.vm.defaultSettings,
...job.settings[''],
...job.settings[schedule.id],
}
if (jobSettings.checkpointSnapshot === true) {
await app.checkFeatureAuthorization('BACKUP.WITH_RAM')
}
}
async deleteBackupNgJob(id) {
const app = this._app
const [schedules] = await Promise.all([app.getAllSchedules(), app.getJob(id, 'backup')])

View File

@ -161,6 +161,10 @@ export default class {
if (!remote.enabled) {
throw new Error('remote is disabled')
}
const parsedRemote = parse(remote.url)
if (parsedRemote.type === 's3') {
await this._app.checkFeatureAuthorization('BACKUP.S3')
}
return remote
}