mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(xo-server): support bundle in featureCheck (#9769)
This commit is contained in:
@@ -56,6 +56,27 @@ export type XapiConnection = Xapi & {
|
||||
sessionId: string
|
||||
_url?: { protocol: string; hostname: string; port?: string }
|
||||
}
|
||||
|
||||
type FeatureCode =
|
||||
| 'BACKUP.DELTA'
|
||||
| 'BACKUP.DELTA_REPLICATION'
|
||||
| 'BACKUP.FULL'
|
||||
| 'BACKUP.HEALTHCHECK'
|
||||
| 'BACKUP.METADATA'
|
||||
| 'BACKUP.MIRROR'
|
||||
| 'BACKUP.WITH_RAM'
|
||||
| 'BACKUP.SMART_BACKUP'
|
||||
| 'BACKUP.S3'
|
||||
| 'DOCKER'
|
||||
| 'EXPORT.XVA'
|
||||
| 'LIST_MISSING_PATCHES'
|
||||
| 'POOL_EMERGENCY_SHUTDOWN'
|
||||
| 'RBAC'
|
||||
| 'ROLLING_POOL_UPDATE'
|
||||
| 'ROLLING_POOL_REBOOT'
|
||||
| 'WARM_MIGRATION'
|
||||
| 'PLUGIN.OPENMETRICS'
|
||||
|
||||
type XapiRecordByXapiXoRecord = {
|
||||
gpuGroup: XenApiGpuGroupWrapped
|
||||
host: XenApiHostWrapped
|
||||
@@ -184,7 +205,7 @@ export type XoApp = {
|
||||
opts?: { bypassOtp?: boolean; bypassTaskCreation?: boolean }
|
||||
) => Promise<{ bypassOtp: boolean; expiration: number; user: XoUser }>
|
||||
/* Throw if no authorization */
|
||||
checkFeatureAuthorization(featureCode: string): Promise<void>
|
||||
checkFeatureAuthorization(featureCode: FeatureCode): Promise<void>
|
||||
/* connect a server (XCP-ng/XenServer) */
|
||||
connectXenServer(id: XoServer['id']): Promise<void>
|
||||
// TODO: replace all XoAclBasePrivilege with a more strict type. (discriminate union)
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
- @xen-orchestra/qcow2 patch
|
||||
- @xen-orchestra/rest-api minor
|
||||
- @xen-orchestra/xapi minor
|
||||
- xo-common minor
|
||||
- xo-server minor
|
||||
- xo-server-sdn-controller patch
|
||||
- xo-web patch
|
||||
|
||||
@@ -206,11 +206,13 @@ exports.incorrectState = create(25, ({ actual, expected, object, property }) =>
|
||||
message: 'incorrect state',
|
||||
}))
|
||||
|
||||
exports.featureUnauthorized = create(26, ({ featureCode, currentPlan, minPlan }) => ({
|
||||
exports.featureUnauthorized = create(26, ({ featureCode, currentPlan, minPlan, currentBundle, allowedBundles }) => ({
|
||||
data: {
|
||||
featureCode,
|
||||
currentPlan,
|
||||
minPlan,
|
||||
currentBundle,
|
||||
allowedBundles
|
||||
},
|
||||
message: 'feature Unauthorized',
|
||||
}))
|
||||
|
||||
@@ -237,7 +237,7 @@ export default class {
|
||||
* @returns {Promise<XoAclRole>}
|
||||
*/
|
||||
async createAclV2Role(role) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
return this.#roleDb.add(role)
|
||||
}
|
||||
@@ -249,7 +249,7 @@ export default class {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async deleteAclV2Role(id, { force = false } = {}) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
const role = await this.getAclV2Role(id)
|
||||
|
||||
@@ -282,7 +282,7 @@ export default class {
|
||||
* @returns {Promise<XoAclRole>}
|
||||
*/
|
||||
async updateAclV2Role(id, { name, description }, { force = false } = {}) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
const role = await this.getAclV2Role(id)
|
||||
if (!force && 'isTemplate' in role) {
|
||||
@@ -308,7 +308,7 @@ export default class {
|
||||
* @returns {Promise<XoAclRole>}
|
||||
*/
|
||||
async getAclV2Role(id) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
const role = await this.#roleDb.first(id)
|
||||
if (role === undefined) {
|
||||
@@ -322,7 +322,7 @@ export default class {
|
||||
* @returns {Promise<XoAclRole[]>}
|
||||
*/
|
||||
async getAclV2Roles() {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
// @ts-ignore typed as Promise<void>...
|
||||
return this.#roleDb.get()
|
||||
@@ -342,7 +342,7 @@ export default class {
|
||||
* @returns {Promise<Privilege>}
|
||||
*/
|
||||
async createAclV2Privilege({ action, selector, effect = 'allow', resource, roleId }, { force = false } = {}) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
const role = await this.getAclV2Role(roleId)
|
||||
if (!force && 'isTemplate' in role) {
|
||||
@@ -360,7 +360,7 @@ export default class {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async deleteAclV2Privilege(id, { force = false } = {}) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
const privilege = await this.getAclV2Privilege(id)
|
||||
const role = await this.getAclV2Role(privilege.roleId)
|
||||
@@ -383,7 +383,7 @@ export default class {
|
||||
* @returns {Promise<Privilege>}
|
||||
*/
|
||||
async updateAclV2Privilege(id, { action, selector, effect, resource }) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
const privilege = await this.getAclV2Privilege(id)
|
||||
const role = await this.getAclV2Role(privilege.roleId)
|
||||
@@ -421,7 +421,7 @@ export default class {
|
||||
* @returns {Promise<Privilege>}
|
||||
*/
|
||||
async getAclV2Privilege(id) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
const privilege = await this.#privilegeDb.first(id)
|
||||
if (privilege === undefined) {
|
||||
@@ -435,7 +435,7 @@ export default class {
|
||||
* @returns {Promise<Privilege[]>}
|
||||
*/
|
||||
async getAclV2Privileges() {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
// @ts-ignore typed as Promise<void>...
|
||||
return this.#privilegeDb.get()
|
||||
@@ -452,7 +452,7 @@ export default class {
|
||||
* @returns {Promise<UserRole>}
|
||||
*/
|
||||
async addAclV2UserRole(userId, roleId) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
/**
|
||||
* @type {UserRole[]}
|
||||
@@ -484,7 +484,7 @@ export default class {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async deleteAclV2UserRole(userId, roleId) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
/**
|
||||
* @type {UserRole[]}
|
||||
@@ -512,7 +512,7 @@ export default class {
|
||||
* @returns {Promise<GroupRole>}
|
||||
*/
|
||||
async addAclV2GroupRole(groupId, roleId) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
/**
|
||||
* @type {GroupRole[]}
|
||||
@@ -544,7 +544,7 @@ export default class {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async deleteAclV2GroupRole(groupId, roleId) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
/**
|
||||
* @type {GroupRole[]}
|
||||
*/
|
||||
@@ -566,7 +566,7 @@ export default class {
|
||||
* @returns {Promise<Privilege[]>}
|
||||
*/
|
||||
async getAclV2RolePrivileges(roleId) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
const role = await this.getAclV2Role(roleId)
|
||||
return this.#privilegeDb._get({ roleId: role.id })
|
||||
@@ -577,7 +577,7 @@ export default class {
|
||||
* @returns {Promise<XoAclRole[]>}
|
||||
*/
|
||||
async getAclV2UserRoles(userId) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
/** @type {XoUser} */
|
||||
const user = await this._app.getUser(userId)
|
||||
@@ -596,7 +596,7 @@ export default class {
|
||||
* @returns {Promise<Privilege[]>}
|
||||
*/
|
||||
async getAclV2UserPrivileges(userId) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
const roles = await this.getAclV2UserRoles(userId)
|
||||
return (await Promise.all(roles.map(role => this.getAclV2RolePrivileges(role.id)))).flat()
|
||||
@@ -607,7 +607,7 @@ export default class {
|
||||
* @returns {Promise<XoAclRole[]>}
|
||||
*/
|
||||
async getAclV2GroupRoles(groupId) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
/** @type {GroupRole[]} */
|
||||
const dbGroupRoles = await this.#groupRoleDb._get({ groupId })
|
||||
@@ -623,7 +623,7 @@ export default class {
|
||||
* @returns {Promise<XoAclRole['id']>}
|
||||
*/
|
||||
async copyAclV2Role(roleId, params = {}) {
|
||||
await this._app.checkFeatureAuthorization('ACL')
|
||||
await this._app.checkFeatureAuthorization('RBAC')
|
||||
|
||||
const role = await this.getAclV2Role(roleId)
|
||||
const privileges = await this.getAclV2RolePrivileges(roleId)
|
||||
|
||||
@@ -11,6 +11,13 @@ const STARTER = 2
|
||||
const ENTERPRISE = 3
|
||||
const PREMIUM = 4
|
||||
|
||||
// https://git.vates.tech/vates/www-xo/src/branch/main/src/productNames.js#L230
|
||||
const BUNDLE_ESSENTIAL = 'bundle-essential'
|
||||
const BUNDLE_ESSENTIAL_PLUS = 'bundle-essential-plus'
|
||||
const BUNDLE_ENTERPRISE = 'bundle-enterprise'
|
||||
const BUNDLE_PRO = 'bundle-pro'
|
||||
const BUNDLE_X1 = 'bundle-x1'
|
||||
|
||||
export const PLANS = {
|
||||
free: FREE,
|
||||
starter: STARTER,
|
||||
@@ -18,8 +25,23 @@ export const PLANS = {
|
||||
premium: PREMIUM,
|
||||
}
|
||||
|
||||
export const BUNDLES = {
|
||||
essential: BUNDLE_ESSENTIAL,
|
||||
essentialPlus: BUNDLE_ESSENTIAL_PLUS,
|
||||
enterprise: BUNDLE_ENTERPRISE,
|
||||
pro: BUNDLE_PRO,
|
||||
x1: BUNDLE_X1,
|
||||
}
|
||||
|
||||
const BUNDLE_TO_PLAN = {
|
||||
[BUNDLE_ESSENTIAL]: STARTER,
|
||||
[BUNDLE_ESSENTIAL_PLUS]: PREMIUM,
|
||||
[BUNDLE_PRO]: ENTERPRISE,
|
||||
[BUNDLE_ENTERPRISE]: PREMIUM,
|
||||
[BUNDLE_X1]: STARTER,
|
||||
}
|
||||
|
||||
const AUTHORIZATIONS = {
|
||||
ACL: ENTERPRISE,
|
||||
BACKUP: {
|
||||
DELTA: STARTER,
|
||||
DELTA_REPLICATION: ENTERPRISE,
|
||||
@@ -45,6 +67,11 @@ const AUTHORIZATIONS = {
|
||||
},
|
||||
}
|
||||
|
||||
// features: https://vates.tech/en/pricing-and-support/
|
||||
const BUNDLE_AUTHORIZATIONS = {
|
||||
RBAC: [BUNDLE_ESSENTIAL_PLUS, BUNDLE_PRO, BUNDLE_ENTERPRISE],
|
||||
}
|
||||
|
||||
export default class Authorization {
|
||||
#app
|
||||
constructor(app) {
|
||||
@@ -52,8 +79,22 @@ export default class Authorization {
|
||||
}
|
||||
|
||||
#getMinPlan(featureCode) {
|
||||
const minPlan = get(AUTHORIZATIONS, featureCode)
|
||||
assert.notEqual(minPlan, undefined, `${featureCode} is not defined in the AUTHORIZATIONS object`)
|
||||
let minPlan = get(AUTHORIZATIONS, featureCode)
|
||||
// If `featureCode` does not exist in the legacy `AUTHORIZATIONS` object, try to find the "equivalent" in the `BUNDLE_AUTHORIZATIONS` object
|
||||
if (minPlan === undefined) {
|
||||
const bundles = get(BUNDLE_AUTHORIZATIONS, featureCode)
|
||||
assert.notEqual(
|
||||
bundles,
|
||||
undefined,
|
||||
`${featureCode} is not defined in either AUTHORIZATIONS or BUNDLE_AUTHORIZATIONS object`
|
||||
)
|
||||
minPlan = bundles
|
||||
.map(bundle => BUNDLE_TO_PLAN[bundle])
|
||||
.sort()
|
||||
.shift()
|
||||
}
|
||||
|
||||
assert.notEqual(minPlan, undefined, `${featureCode} is not defined in the BUNDLE_AUTHORIZATIONS object`)
|
||||
return minPlan
|
||||
}
|
||||
|
||||
@@ -72,20 +113,58 @@ export default class Authorization {
|
||||
return PLANS[plan]
|
||||
}
|
||||
|
||||
async #getCurrentBundleId() {
|
||||
const now = Date.now()
|
||||
const xoaLicences = await retry(() => this.#app.getSelfLicenses(), {
|
||||
when: error => error.message?.includes('invalid status connecting'),
|
||||
onRetry(error) {
|
||||
log.warn('XOA connection not ready, retrying', {
|
||||
attempt: this.attemptNumber,
|
||||
delay: this.delay,
|
||||
error,
|
||||
})
|
||||
},
|
||||
})
|
||||
const activeBundleLicenses = xoaLicences?.filter(
|
||||
({ expires, bundleInfo }) => (expires === undefined || expires > now) && bundleInfo !== undefined
|
||||
)
|
||||
if (activeBundleLicenses === undefined || activeBundleLicenses.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return activeBundleLicenses.sort(
|
||||
(a, b) => (BUNDLE_TO_PLAN[b.bundleInfo.id] ?? 0) - (BUNDLE_TO_PLAN[a.bundleInfo.id] ?? 0)
|
||||
)[0].bundleInfo.id
|
||||
}
|
||||
|
||||
async checkFeatureAuthorization(featureCode) {
|
||||
if (this.#app.getXoaPlan === undefined) {
|
||||
// source user => everything is open
|
||||
return
|
||||
}
|
||||
|
||||
const minPlan = this.#getMinPlan(featureCode)
|
||||
const currentPlan = await this.#getCurrentPlan()
|
||||
if (currentPlan < minPlan) {
|
||||
throw featureUnauthorized({
|
||||
featureCode,
|
||||
currentPlan,
|
||||
minPlan,
|
||||
})
|
||||
const bundleId = await this.#getCurrentBundleId()
|
||||
if (bundleId !== undefined) {
|
||||
const allowedBundles = get(BUNDLE_AUTHORIZATIONS, featureCode)
|
||||
if (allowedBundles === undefined) {
|
||||
const minPlan = this.#getMinPlan(featureCode)
|
||||
if (BUNDLE_TO_PLAN[bundleId] >= minPlan) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (allowedBundles.includes(bundleId)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
throw featureUnauthorized({ featureCode, currentBundle: bundleId, allowedBundles })
|
||||
} else {
|
||||
// fallback to legacy feature check
|
||||
const minPlan = this.#getMinPlan(featureCode)
|
||||
const currentPlan = await this.#getCurrentPlan()
|
||||
if (currentPlan < minPlan) {
|
||||
throw featureUnauthorized({ featureCode, currentPlan, minPlan })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
140
packages/xo-server/src/xo-mixins/authorization.test.mjs
Normal file
140
packages/xo-server/src/xo-mixins/authorization.test.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
import assert from 'assert/strict'
|
||||
import test from 'node:test'
|
||||
import Authorization, { BUNDLES, PLANS } from './authorization.mjs'
|
||||
|
||||
const { describe, it } = test
|
||||
|
||||
const makeApp = ({ plan = PLANS.premium, licenses } = {}) => ({
|
||||
getXoaPlan: async () => plan,
|
||||
getSelfLicenses: async () => licenses,
|
||||
})
|
||||
|
||||
const makeLicense = (bundleId, { expires } = {}) => ({
|
||||
bundleInfo: { id: bundleId },
|
||||
expires,
|
||||
})
|
||||
|
||||
describe('Authorization', function () {
|
||||
describe('checkFeatureAuthorization', function () {
|
||||
describe('source user (no getXoaPlan)', function () {
|
||||
it('should allow any feature', async function () {
|
||||
const auth = new Authorization({})
|
||||
await assert.doesNotReject(() => auth.checkFeatureAuthorization('RBAC'))
|
||||
await assert.doesNotReject(() => auth.checkFeatureAuthorization('WARM_MIGRATION'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('unknown feature code', function () {
|
||||
it('should throw an assertion error', async function () {
|
||||
const auth = new Authorization(makeApp({ plan: 'enterprise' }))
|
||||
await assert.rejects(() => auth.checkFeatureAuthorization('UNKNOWN.FEATURE'), { name: 'AssertionError' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('bundle-based authorization', function () {
|
||||
describe('feature defined in BUNDLE_AUTHORIZATIONS (RBAC)', function () {
|
||||
for (const allowedBundle of [BUNDLES.essentialPlus, BUNDLES.pro, BUNDLES.enterprise]) {
|
||||
it(`should allow RBAC with: ${allowedBundle}`, async function () {
|
||||
const auth = new Authorization(makeApp({ licenses: [makeLicense(allowedBundle)] }))
|
||||
await assert.doesNotReject(() => auth.checkFeatureAuthorization('RBAC'))
|
||||
})
|
||||
}
|
||||
|
||||
for (const deniedBundle of [BUNDLES.essential, BUNDLES.x1]) {
|
||||
it(`should deny RBAC with ${deniedBundle}`, async function () {
|
||||
const auth = new Authorization(makeApp({ licenses: [makeLicense(deniedBundle)] }))
|
||||
await assert.rejects(() => auth.checkFeatureAuthorization('RBAC'))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// test plan -> bundle migration
|
||||
describe('feature not in BUNDLE_AUTHORIZATIONS (plan comparison fallback)', function () {
|
||||
it('should allow when bundle plan meets the minimum', async function () {
|
||||
const auth = new Authorization(makeApp({ licenses: [makeLicense('bundle-essential')] }))
|
||||
await assert.doesNotReject(() => auth.checkFeatureAuthorization('BACKUP.DELTA'))
|
||||
})
|
||||
|
||||
it('should deny when bundle plan is below minimum', async function () {
|
||||
const auth = new Authorization(makeApp({ licenses: [makeLicense('bundle-essential')] }))
|
||||
await assert.rejects(() => auth.checkFeatureAuthorization('WARM_MIGRATION'))
|
||||
})
|
||||
|
||||
it('should allow when bundle plan exactly meets minimum', async function () {
|
||||
const auth = new Authorization(makeApp({ licenses: [makeLicense('bundle-essential-plus')] }))
|
||||
await assert.doesNotReject(() => auth.checkFeatureAuthorization('WARM_MIGRATION'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('multiple licenses', function () {
|
||||
it('should use the highest tier bundle when several are active', async function () {
|
||||
const auth = new Authorization(
|
||||
makeApp({
|
||||
licenses: [makeLicense('bundle-essential'), makeLicense('bundle-essential-plus')],
|
||||
})
|
||||
)
|
||||
// RBAC requires bundle-essential-plus → should pass because it picks the best bundle
|
||||
await assert.doesNotReject(() => auth.checkFeatureAuthorization('RBAC'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('license expiry', function () {
|
||||
// IMO, when plan check is no more used, we should throw if licence has expired
|
||||
it('should ignore expired licenses and fall back to plan check (to avoid breaking changes)', async function () {
|
||||
const auth = new Authorization(
|
||||
makeApp({
|
||||
plan: 'free',
|
||||
licenses: [makeLicense('bundle-enterprise', { expires: Date.now() - 1000 })],
|
||||
})
|
||||
)
|
||||
// No active bundle → legacy plan check → free < enterprise → deny
|
||||
await assert.rejects(() => auth.checkFeatureAuthorization('RBAC'))
|
||||
})
|
||||
|
||||
it('should use non-expired licenses', async function () {
|
||||
const auth = new Authorization(
|
||||
makeApp({ licenses: [makeLicense('bundle-essential', { expires: Date.now() + 100_000 })] })
|
||||
)
|
||||
await assert.doesNotReject(() => auth.checkFeatureAuthorization('BACKUP.DELTA'))
|
||||
})
|
||||
|
||||
it('should treat licenses with no expiry as active', async function () {
|
||||
const auth = new Authorization(makeApp({ licenses: [makeLicense('bundle-essential-plus')] }))
|
||||
await assert.doesNotReject(() => auth.checkFeatureAuthorization('WARM_MIGRATION'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('legacy plan-based authorization (no active bundle)', function () {
|
||||
for (const [plan, feature, shouldPass] of [
|
||||
['starter', 'BACKUP.DELTA', true],
|
||||
['starter', 'RBAC', false],
|
||||
['enterprise', 'RBAC', true],
|
||||
['enterprise', 'WARM_MIGRATION', false],
|
||||
['premium', 'WARM_MIGRATION', true],
|
||||
['free', 'BACKUP.DELTA', false],
|
||||
]) {
|
||||
it(`${plan} plan + ${feature} → ${shouldPass ? 'allow' : 'deny'}`, async function () {
|
||||
const auth = new Authorization(makeApp({ plan }))
|
||||
if (shouldPass) {
|
||||
await assert.doesNotReject(() => auth.checkFeatureAuthorization(feature))
|
||||
} else {
|
||||
await assert.rejects(() => auth.checkFeatureAuthorization(feature))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasFeatureAuthorization', function () {
|
||||
it('should return true when feature is authorized', async function () {
|
||||
const auth = new Authorization(makeApp({ licenses: [makeLicense(BUNDLES.enterprise)] }))
|
||||
assert.equal(await auth.hasFeatureAuthorization('WARM_MIGRATION'), true)
|
||||
})
|
||||
|
||||
it('should return false when feature is not authorized', async function () {
|
||||
const auth = new Authorization(makeApp({ licenses: [makeLicense(BUNDLES.essential)] }))
|
||||
assert.equal(await auth.hasFeatureAuthorization('RBAC'), false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user