feat(xo-server): server side authorization (#6107)
This commit is contained in:
parent
0b41a2b132
commit
8ce1b4bf71
@ -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',
|
||||
}))
|
||||
|
67
packages/xo-server/src/xo-mixins/authorization.mjs
Normal file
67
packages/xo-server/src/xo-mixins/authorization.mjs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -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')])
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user