@getanthill/datastore
Version:
Event-Sourced Datastore
1,581 lines (1,532 loc) • 40.6 kB
text/typescript
import Ajv from 'ajv';
import Authz from './authz';
import setup from '../../test/setup';
import {
AUTHORIZATION_VERB_ALLOW,
AUTHORIZATION_VERB_DENY,
PolicyVerbs,
PolicyVerb,
} from '../typings/authorizations';
describe('Authz', () => {
beforeEach(async () => {
jest.restoreAllMocks();
});
describe('#getScopes', () => {
it('returns all possible scopes', () => {
expect(Authz.getScopes(['a', 'b', 'c'])).toEqual([
['a'],
['a', 'b'],
['a', 'b', 'c'],
]);
});
it('returns empty array on empty scope', () => {
expect(Authz.getScopes([])).toEqual([]);
});
it('returns single array of scopes on single level scope', () => {
expect(Authz.getScopes(['a'])).toEqual([['a']]);
});
});
describe('#areRulesValidated', () => {
it('returns true if all scopes are valid', () => {
expect(
Authz.areRulesValidated([
{
action: {
is_valid: true,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
]),
).toEqual(true);
});
it('returns false if action scope is invalid', () => {
expect(
Authz.areRulesValidated([
{
action: {
is_valid: false,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
]),
).toEqual(false);
});
it('returns false if subject scope is invalid', () => {
expect(
Authz.areRulesValidated([
{
action: {
is_valid: true,
errors: [],
},
subject: {
is_valid: false,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
]),
).toEqual(false);
});
it('returns false if object scope is invalid', () => {
expect(
Authz.areRulesValidated([
{
action: {
is_valid: true,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: false,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
]),
).toEqual(false);
});
it('returns false if context scope is invalid', () => {
expect(
Authz.areRulesValidated([
{
action: {
is_valid: true,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: false,
errors: [],
},
},
]),
).toEqual(false);
});
it('returns false if second action scope is invalid', () => {
expect(
Authz.areRulesValidated([
{
action: {
is_valid: true,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
{
action: {
is_valid: false,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
]),
).toEqual(false);
});
});
describe('#isAllowed', () => {
it('returns true if the policy is validated and verb is `allow`', () => {
expect(
Authz.isAllowed([
{
obligations: [],
verb: AUTHORIZATION_VERB_ALLOW as PolicyVerbs.Allow,
validations: [
{
action: {
is_valid: true,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
],
},
]),
).toEqual(true);
});
it('returns false if the policy is validated and verb is `deny`', () => {
expect(
Authz.isAllowed([
{
obligations: [],
verb: AUTHORIZATION_VERB_DENY as PolicyVerbs.Deny,
validations: [
{
action: {
is_valid: true,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
],
},
]),
).toEqual(false);
});
it('returns false if the policy is not validated but the verb is `allow`', () => {
expect(
Authz.isAllowed([
{
obligations: [],
verb: AUTHORIZATION_VERB_ALLOW as PolicyVerbs.Allow,
validations: [
{
action: {
is_valid: false,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
],
},
]),
).toEqual(false);
});
it('returns true if the policy is not validated but the verb is `deny`', () => {
expect(
Authz.isAllowed([
{
obligations: [],
verb: AUTHORIZATION_VERB_DENY as PolicyVerbs.Deny,
validations: [
{
action: {
is_valid: false,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
],
},
]),
).toEqual(true);
});
it('returns false if a policy is already validated and verb is `deny`', () => {
expect(
Authz.isAllowed([
{
obligations: [],
verb: AUTHORIZATION_VERB_DENY as PolicyVerbs.Deny,
validations: [
{
action: {
is_valid: true,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
],
},
{
obligations: [],
verb: AUTHORIZATION_VERB_ALLOW as PolicyVerbs.Allow,
validations: [
{
action: {
is_valid: true,
errors: [],
},
subject: {
is_valid: true,
errors: [],
},
object: {
is_valid: true,
errors: [],
},
context: {
is_valid: true,
errors: [],
},
},
],
},
]),
).toEqual(false);
});
});
describe('#validateRules', () => {
let validator;
beforeEach(() => {
validator = new Ajv({
strict: false,
useDefaults: false,
coerceTypes: true,
});
});
it('is invalid if no rule is defined', () => {
const validations = Authz.validateRules(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [],
object: [],
context: [],
},
[],
);
expect(Authz.areRulesValidated(validations)).toEqual(false);
});
it('is valid if the subject is matching a rule', () => {
const validations = Authz.validateRules(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [
{
is_enabled: true,
scope: ['subject'],
description: 'Subject is having attribute `firstname=john`',
attribute: 'firstname',
value: 'john',
},
],
object: [],
context: [],
},
[
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
attribute: {
type: 'string',
enum: ['firstname'],
},
value: {
type: 'string',
enum: ['john'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
);
expect(Authz.areRulesValidated(validations)).toEqual(true);
});
it('is invalid if the subject does not match a rule', () => {
const validations = Authz.validateRules(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [
{
is_enabled: true,
scope: ['subject'],
description: 'Subject is having attribute `firstname=john`',
attribute: 'firstname',
value: 'john',
},
],
object: [],
context: [],
},
[
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=alice`', // <-- Not john
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
attribute: {
type: 'string',
enum: ['firstname'],
},
value: {
type: 'string',
enum: ['alice'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
);
expect(Authz.areRulesValidated(validations)).toEqual(false);
});
it('compiles and keeps compiled version of schema in memory if $id is available', () => {
const validations = Authz.validateRules(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [
{
is_enabled: true,
scope: ['subject'],
description: 'Subject is having attribute `firstname=john`',
attribute: 'firstname',
value: 'john',
},
],
object: [],
context: [],
},
[
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
$id: 'subject',
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
attribute: {
type: 'string',
enum: ['firstname'],
},
value: {
type: 'string',
enum: ['john'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
);
expect(Authz.areRulesValidated(validations)).toEqual(true);
expect(validator.getSchema('subject')).not.toBeUndefined();
});
it('is valid if the subject is matching all the rules', () => {
const validations = Authz.validateRules(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [
{
is_enabled: true,
scope: ['subject'],
description: 'Subject is having attribute `firstname=john`',
attribute: 'firstname',
value: 'john',
},
],
object: [],
context: [],
},
[
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
attribute: {
type: 'string',
enum: ['firstname'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
value: {
type: 'string',
enum: ['john'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
);
expect(Authz.areRulesValidated(validations)).toEqual(true);
});
it('is invalid if the subject does not match one rules among others', () => {
const validations = Authz.validateRules(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [
{
is_enabled: true,
scope: ['subject'],
description: 'Subject is having attribute `firstname=john`',
attribute: 'firstname',
value: 'john',
},
],
object: [],
context: [],
},
[
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
attribute: {
type: 'string',
enum: ['firstname'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
value: {
type: 'string',
enum: ['alice'], // <-- Not john
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
);
expect(Authz.areRulesValidated(validations)).toEqual(false);
});
});
describe('#validatePolicy', () => {
let validator;
beforeEach(() => {
validator = new Ajv({
strict: false,
useDefaults: false,
coerceTypes: true,
});
});
it('returns an `allow` decision if attributes are matching the policy rules', () => {
const decision = Authz.validatePolicy(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [
{
is_enabled: true,
scope: ['subject'],
description: 'Subject is having attribute `firstname=john`',
attribute: 'firstname',
value: 'john',
},
],
object: [],
context: [],
},
{
name: 'policy',
description: 'policy description',
is_enabled: true,
obligations: [],
scope: ['scope'],
verb: AUTHORIZATION_VERB_ALLOW as PolicyVerb,
rules: [
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
attribute: {
type: 'string',
enum: ['firstname'],
},
value: {
type: 'string',
enum: ['john'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
},
);
expect(decision).toEqual({
obligations: [],
validations: [
{
action: {
errors: [],
is_valid: true,
},
context: {
errors: [],
is_valid: true,
},
object: {
errors: [],
is_valid: true,
},
subject: {
errors: [],
is_valid: true,
},
},
],
verb: 'allow',
});
});
it('returns an `deny` decision if attributes are not matching the policy rules', () => {
const decision = Authz.validatePolicy(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [
{
is_enabled: true,
scope: ['subject'],
description: 'Subject is having attribute `firstname=john`',
attribute: 'firstname',
value: 'john',
},
],
object: [],
context: [],
},
{
name: 'policy',
description: 'policy description',
is_enabled: true,
obligations: [],
scope: ['scope'],
verb: AUTHORIZATION_VERB_DENY as PolicyVerb,
rules: [
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
attribute: {
type: 'string',
enum: ['firstname'],
},
value: {
type: 'string',
not: { enum: ['alice'] },
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
},
);
expect(decision).toEqual({
obligations: [],
validations: [
{
action: {
errors: [],
is_valid: true,
},
context: {
errors: [],
is_valid: true,
},
object: {
errors: [],
is_valid: true,
},
subject: {
errors: [],
is_valid: true,
},
},
],
verb: 'deny',
});
});
it('returns an `allow` decision if the policy rules are not matching', () => {
const decision = Authz.validatePolicy(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [
{
is_enabled: true,
scope: ['subject'],
description: 'Subject is having attribute `firstname=john`',
attribute: 'firstname',
value: 'john',
},
],
object: [],
context: [],
},
{
name: 'policy',
description: 'policy description',
is_enabled: true,
obligations: [],
scope: ['scope'],
verb: AUTHORIZATION_VERB_DENY as PolicyVerb,
rules: [
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
attribute: {
type: 'string',
enum: ['firstname'],
},
value: {
type: 'string',
enum: ['alice'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
},
);
expect(decision).toEqual({
obligations: [],
validations: [
{
action: {
errors: [],
is_valid: true,
},
context: {
errors: [],
is_valid: true,
},
object: {
errors: [],
is_valid: true,
},
subject: {
errors: [
{
instancePath: '/_attributes/0/value',
keyword: 'enum',
message: 'must be equal to one of the allowed values',
params: {
allowedValues: ['alice'],
},
schemaPath:
'#/properties/_attributes/items/anyOf/0/properties/value/enum',
},
{
instancePath: '/_attributes/0',
keyword: 'anyOf',
message: 'must match a schema in anyOf',
params: {},
schemaPath: '#/properties/_attributes/items/anyOf',
},
],
is_valid: false,
},
},
],
verb: 'allow',
});
});
});
describe('#validatePolicies', () => {
let validator;
beforeEach(() => {
validator = new Ajv({
strict: false,
useDefaults: false,
coerceTypes: true,
});
});
it('returns a `allow` decision if no policy is provided and `noPolicyVerb=allow`', () => {
expect(
Authz.validatePolicies(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [],
object: [],
context: [],
},
[],
{
noPolicyVerb: AUTHORIZATION_VERB_ALLOW as PolicyVerb,
},
),
).toMatchObject({
verb: 'allow',
});
});
it('returns a `deny` decision if no policy is provided and `noPolicyVerb=deny`', () => {
expect(
Authz.validatePolicies(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [],
object: [],
context: [],
},
[],
{
noPolicyVerb: AUTHORIZATION_VERB_DENY as PolicyVerb,
},
),
).toMatchObject({
verb: 'deny',
});
});
it('returns an `allow` decision if attributes are matching all the policy rules', () => {
const decision = Authz.validatePolicies(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [
{
is_enabled: true,
scope: ['subject'],
description: 'Subject is having attribute `firstname=john`',
attribute: 'firstname',
value: 'john',
},
],
object: [],
context: [],
},
[
{
name: 'required_firstname',
description: 'Firstname attribute must exist',
is_enabled: true,
obligations: [],
scope: ['scope'],
verb: AUTHORIZATION_VERB_ALLOW as PolicyVerb,
rules: [
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
attribute: {
type: 'string',
enum: ['firstname'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
},
{
name: 'required_john',
description: 'Attribute value John must be found',
is_enabled: true,
obligations: [],
scope: ['scope'],
verb: AUTHORIZATION_VERB_ALLOW as PolicyVerb,
rules: [
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
value: {
type: 'string',
enum: ['john'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
},
],
{
noPolicyVerb: AUTHORIZATION_VERB_ALLOW as PolicyVerb,
},
);
expect(decision).toEqual({
obligations: [],
validations: [
{
action: {
errors: [],
is_valid: true,
},
context: {
errors: [],
is_valid: true,
},
object: {
errors: [],
is_valid: true,
},
subject: {
errors: [],
is_valid: true,
},
},
{
action: {
errors: [],
is_valid: true,
},
context: {
errors: [],
is_valid: true,
},
object: {
errors: [],
is_valid: true,
},
subject: {
errors: [],
is_valid: true,
},
},
],
verb: 'allow',
});
});
it('returns an `deny` decision if attributes are not matching the policies expected ones', () => {
const decision = Authz.validatePolicies(
validator,
{
action: {
scope: ['action'],
},
subject: {
scope: ['subject'],
},
object: {
scope: ['object'],
},
context: {
scope: ['context'],
},
},
{
action: [],
subject: [
{
is_enabled: true,
scope: ['subject'],
description: 'Subject is having attribute `firstname=john`',
attribute: 'firstname',
value: 'john',
},
],
object: [],
context: [],
},
[
{
name: 'required_firstname',
description: 'Firstname attribute must exist',
is_enabled: true,
obligations: [],
scope: ['scope'],
verb: AUTHORIZATION_VERB_DENY as PolicyVerb,
rules: [
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
not: { required: ['missing'] },
},
],
},
},
},
},
object: {},
context: {},
},
],
},
{
name: 'required_john',
description: 'Attribute value John must be found',
is_enabled: true,
obligations: [],
scope: ['scope'],
verb: AUTHORIZATION_VERB_ALLOW as PolicyVerb,
rules: [
{
description: '',
is_enabled: true,
name: 'Subject must have attribute `firstname=john`',
action: {},
subject: {
type: 'object',
required: ['_attributes'],
properties: {
_attributes: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
value: {
type: 'string',
enum: ['john'],
},
},
},
],
},
},
},
},
object: {},
context: {},
},
],
},
],
{
noPolicyVerb: AUTHORIZATION_VERB_ALLOW as PolicyVerb,
},
);
expect(decision).toEqual({
obligations: [],
validations: [
{
action: {
errors: [],
is_valid: true,
},
context: {
errors: [],
is_valid: true,
},
object: {
errors: [],
is_valid: true,
},
subject: {
errors: [],
is_valid: true,
},
},
{
action: {
errors: [],
is_valid: true,
},
context: {
errors: [],
is_valid: true,
},
object: {
errors: [],
is_valid: true,
},
subject: {
errors: [],
is_valid: true,
},
},
],
verb: 'deny',
});
});
});
});