Skip to content

Commit

Permalink
feat(actionbasedpolicy): allowing set and get context
Browse files Browse the repository at this point in the history
  • Loading branch information
roggervalf committed Oct 17, 2020
1 parent 579481c commit 3420d87
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 92 deletions.
139 changes: 82 additions & 57 deletions src/ActionBasedPolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,36 @@ describe('ActionBasedPolicy Class', () => {
it("doesn't throw an error", () => {
expect(
() =>
new ActionBasedPolicy([
{
action: ['read', 'write']
}
])
new ActionBasedPolicy({
statements: [
{
action: ['read', 'write']
}
]
})
).not.toThrow();
expect(
() =>
new ActionBasedPolicy({
statements: [
{
notAction: ['write']
}
],
context: {}
})
).not.toThrow();
});

it('returns an ActionBasedPolicy instance', () => {
expect(
new ActionBasedPolicy([
{
action: ['read', 'write']
}
])
new ActionBasedPolicy({
statements: [
{
action: ['read', 'write']
}
]
})
).toBeInstanceOf(ActionBasedPolicy);
});
});
Expand All @@ -28,10 +43,10 @@ describe('ActionBasedPolicy Class', () => {
it('returns those statements', () => {
const statements = [
{
action: ['read']
action: 'read'
}
];
const policy = new ActionBasedPolicy(statements);
const policy = new ActionBasedPolicy({ statements });
const exportedStatements = policy.getStatements();
expect(exportedStatements).toMatchObject(statements);
expect(exportedStatements[0].sid).not.toBeFalsy();
Expand All @@ -40,11 +55,13 @@ describe('ActionBasedPolicy Class', () => {

describe('when match actions', () => {
it('returns true or false', () => {
const policy = new ActionBasedPolicy([
{
action: ['read']
}
]);
const policy = new ActionBasedPolicy({
statements: [
{
action: ['read']
}
]
});

expect(
policy.evaluate({
Expand All @@ -61,11 +78,13 @@ describe('ActionBasedPolicy Class', () => {

describe('when match not actions', () => {
it('returns true or false', () => {
const policy = new ActionBasedPolicy([
{
notAction: 'read'
}
]);
const policy = new ActionBasedPolicy({
statements: [
{
notAction: 'read'
}
]
});

expect(
policy.evaluate({
Expand All @@ -82,19 +101,21 @@ describe('ActionBasedPolicy Class', () => {

describe('when match based on context', () => {
it('returns true or false', () => {
const policy = new ActionBasedPolicy([
{
action: ['getUser/${user.id}', 'updateUser/${user.id}']
},
{
action: 'getAllProjects'
}
]);
const policy = new ActionBasedPolicy({
statements: [
{
action: ['getUser/${user.id}', 'updateUser/${user.id}']
},
{
action: 'getAllProjects'
}
],
context: { user: { id: 123 } }
});

expect(
policy.evaluate({
action: 'getUser/123',
context: { user: { id: 123 } }
action: 'getUser/123'
})
).toBe(true);
expect(
Expand Down Expand Up @@ -123,14 +144,14 @@ describe('ActionBasedPolicy Class', () => {

describe('when match based on conditions', () => {
it('returns true or false', () => {
const conditions = {
const conditionResolver = {
greaterThan: (data: number, expected: number): boolean => {
return data > expected;
}
};

const policy = new ActionBasedPolicy(
[
const policy = new ActionBasedPolicy({
statements: [
{
action: ['read']
},
Expand All @@ -143,8 +164,8 @@ describe('ActionBasedPolicy Class', () => {
}
}
],
conditions
);
conditionResolver
});

expect(
policy.evaluate({
Expand All @@ -169,16 +190,18 @@ describe('ActionBasedPolicy Class', () => {

describe('can and cannot', () => {
it('can should return false when not found and true for when matched with allow', () => {
const policy = new ActionBasedPolicy([
{
effect: 'allow',
action: [
'createProject',
'getUser/${user.id}',
'updateUser/${user.id}'
]
}
]);
const policy = new ActionBasedPolicy({
statements: [
{
effect: 'allow',
action: [
'createProject',
'getUser/${user.id}',
'updateUser/${user.id}'
]
}
]
});
expect(
policy.can({
action: 'getUser/123',
Expand All @@ -199,16 +222,18 @@ describe('ActionBasedPolicy Class', () => {
});

it('cannot should return false when not found and true for when matched with deny', () => {
const policy = new ActionBasedPolicy([
{
effect: 'deny',
action: [
'createProject',
'getUser/${user.id}',
'updateUser/${user.id}'
]
}
]);
const policy = new ActionBasedPolicy({
statements: [
{
effect: 'deny',
action: [
'createProject',
'getUser/${user.id}',
'updateUser/${user.id}'
]
}
]
});
expect(
policy.cannot({
action: 'getUser/123',
Expand Down
47 changes: 32 additions & 15 deletions src/ActionBasedPolicy.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,67 @@
import {
ActionBasedType,
EvaluateActionBasedInterface,
ConditionResolver
ConditionResolver,
Context
} from './types';
import { ActionBased } from './ActionBasedStatement';
import { Policy } from './Policy';

export class ActionBasedPolicy {
interface ActionBasedPolicyInterface {
statements: ActionBasedType[];
conditionResolver?: ConditionResolver;
context?: Context;
}

export class ActionBasedPolicy extends Policy {
private denyStatements: ActionBased[];
private allowStatements: ActionBased[];
private conditionResolver?: ConditionResolver;
private statements: ActionBasedType[];
constructor(
config: ActionBasedType[],
conditionResolver?: ConditionResolver
) {
const statementInstances = config.map(
statement => new ActionBased(statement)

constructor({
statements,
conditionResolver,
context
}: ActionBasedPolicyInterface) {
super({ context });
const statementInstances = statements.map(
(statement) => new ActionBased(statement)
);
this.allowStatements = statementInstances.filter(
(s) => s.effect === 'allow'
);
this.allowStatements = statementInstances.filter(s => s.effect === 'allow');
this.denyStatements = statementInstances.filter(s => s.effect === 'deny');
this.denyStatements = statementInstances.filter((s) => s.effect === 'deny');
this.conditionResolver = conditionResolver;
this.statements = this.statements = statementInstances.map(statement =>
this.statements = this.statements = statementInstances.map((statement) =>
statement.getStatement()
);
}

getStatements(): ActionBasedType[] {
return this.statements;
}

evaluate({ action, context }: EvaluateActionBasedInterface): boolean {
const args = { action, context };
return !this.cannot(args) && this.can(args);
}

can({ action, context }: EvaluateActionBasedInterface): boolean {
return this.allowStatements.some(s =>
return this.allowStatements.some((s) =>
s.matches({
action,
context,
context: context || this.context,
conditionResolver: this.conditionResolver
})
);
}

cannot({ action, context }: EvaluateActionBasedInterface): boolean {
return this.denyStatements.some(s =>
return this.denyStatements.some((s) =>
s.matches({
action,
context,
context: context || this.context,
conditionResolver: this.conditionResolver
})
);
Expand Down
26 changes: 26 additions & 0 deletions src/Policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Policy } from './Policy';

describe('Policy Class', () => {
it('returns a Policy instance', () => {
expect(new Policy({})).toBeInstanceOf(Policy);
expect(new Policy({ context: { user: { age: 31 } } })).toBeInstanceOf(
Policy
);
});

describe('when getContext', () => {
it('returns context attribute', () => {
const context = { user: { age: 31 } };
expect(new Policy({ context }).getContext()).toBe(context);
});
});

describe('when setContext', () => {
it('sets context attribute', () => {
const context = { user: { age: 31 } };
const policy = new Policy({});
policy.setContext(context);
expect(policy.getContext()).toBe(context);
});
});
});
21 changes: 21 additions & 0 deletions src/Policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PolicyInterface, Context } from './types';

class Policy {
protected context: Context;

constructor({ context }: PolicyInterface) {
if (context) {
this.context = context;
}
}

setContext(context: Context): void {
this.context = context;
}

getContext(): Context {
return this.context;
}
}

export { Policy };
6 changes: 3 additions & 3 deletions src/Statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ class Statement {
conditionResolver
}: MatchConditionInterface): boolean {
return conditionResolver && this.condition && context
? Object.keys(this.condition).every(condition =>
? Object.keys(this.condition).every((condition) =>
Object.keys(this.condition ? this.condition[condition] : {}).every(
path => {
(path) => {
if (this.condition) {
const conditionValues = this.condition[condition][path];
if (conditionValues instanceof Array) {
return conditionValues.some(value =>
return conditionValues.some((value) =>
conditionResolver[condition](
getValueFromPath(context, path),
value
Expand Down
12 changes: 8 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ interface NotResourceBlock {

export type ConditionKey = string | number | boolean;

export interface Context {
[key: string]: ConditionKey | Context | string[] | number[];
}

export interface ConditionMap {
[key: string]: ConditionKey[] | ConditionKey;
}
Expand All @@ -45,6 +49,10 @@ export interface StatementInterface {
condition?: ConditionBlock;
}

export interface PolicyInterface {
context?: Context;
}

export interface DecomposeString {
start: number;
end: number;
Expand All @@ -59,10 +67,6 @@ export interface ConditionResolver {
[key: string]: Resolver;
}

export interface Context {
[key: string]: ConditionKey | Context | string[] | number[];
}

export interface MatchConditionInterface {
context?: Context;
conditionResolver?: ConditionResolver;
Expand Down

0 comments on commit 3420d87

Please sign in to comment.