Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/__fixtures__/contexts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const allowedContext = {
subject: {
id: 1234,
},
doc: {
createdBy: 1234,
},
};

export const deniedContext = {
subject: {
groups: ["group5"],
},
};

export enum Actions {
create = "create",
read = "read",
createDocument = "documents:createDocument",
readDocument = "documents:readDocument",
signDocuments = "sign:documents",
}
28 changes: 28 additions & 0 deletions lib/__fixtures__/parsed-statements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as statements from "./statements";
import { parsePolicyStatement } from "../parsed-policy-statement";

export const GlobAllStatement = parsePolicyStatement(
statements.GlobAllStatement
);
export const GlobEndStatement = parsePolicyStatement(
statements.GlobEndStatement
);
export const GlobStartStatement = parsePolicyStatement(
statements.GlobStartStatement
);

export const BasicAllowStatement = parsePolicyStatement(
statements.BasicAllowStatement
);
export const MultipleActionsStatement = parsePolicyStatement(
statements.MultipleActionsStatement
);
export const ContextualAllowStatement = parsePolicyStatement(
statements.ContextualAllowStatement
);
export const ContextualDenyStatement = parsePolicyStatement(
statements.ContextualDenyStatement
);
export const ContextualGlobStatement = parsePolicyStatement(
statements.ContextualGlobStatement
);
137 changes: 58 additions & 79 deletions lib/__fixtures__/statements.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,72 @@
import { parsePolicyStatement } from "../parsed-policy-statement";
import { PolicyStatement } from "../types";
import { Actions } from "./contexts";

export const GlobAllStatement = parsePolicyStatement(
{
action: "*",
effect: "allow",
constraint: true,
},
"GlobAllStatement"
);

export const GlobStartStatement = parsePolicyStatement(
{
action: "*:documents",
effect: "allow",
constraint: true,
},
"GlobStartStatement"
);
export const GlobAllStatement: PolicyStatement = {
sid: "GlobAllStatement",
action: "*",
effect: "allow",
constraint: true,
};

export const GlobEndStatement = parsePolicyStatement(
{
action: "documents:*",
effect: "allow",
constraint: true,
},
"GlobEndStatement"
);
export const GlobStartStatement: PolicyStatement = {
sid: "GlobStartStatement",
action: "*:documents",
effect: "allow",
constraint: true,
};

export const BasicAllowStatement = parsePolicyStatement(
{
action: "create",
effect: "allow",
constraint: true,
},
"BasicAllowStatement"
);
export const GlobEndStatement: PolicyStatement = {
sid: "GlobEndStatement",
action: "documents:*",
effect: "allow",
constraint: true,
};

export const MultipleActionsStatement = parsePolicyStatement(
{
action: ["create", "read"],
effect: "allow",
constraint: true,
},
"MultipleActionsStatement"
);
export const BasicAllowStatement: PolicyStatement = {
sid: "BasicAllowStatement",
action: Actions.create,
effect: "allow",
constraint: true,
};

export const allowedContext = {
subject: {
id: 1234,
},
doc: {
createdBy: 1234,
},
export const MultipleActionsStatement: PolicyStatement = {
sid: "MultipleActionsStatement",
action: [Actions.create, Actions.read],
effect: "allow",
constraint: true,
};

export const ContextualAllowStatement = parsePolicyStatement(
{
action: "create",
effect: "allow",
constraint: {
"===": [{ var: "subject.id" }, { var: "doc.createdBy" }],
},
export const ContextualAllowStatement: PolicyStatement = {
sid: "ContextualAllowStatement",
action: Actions.create,
effect: "allow",
constraint: {
"===": [{ var: "subject.id" }, { var: "doc.createdBy" }],
},
"ContextualAllowStatement"
);
};

export const deniedContext = {
subject: {
groups: ["group5"],
export const ContextualDenyStatement: PolicyStatement = {
action: Actions.create,
effect: "deny",
sid: "ContextualDenyStatement",
constraint: {
some: [{ var: "subject.groups" }, { in: [{ var: "" }, ["group5"]] }],
},
};

export const ContextualDenyStatement = parsePolicyStatement(
{
action: "create",
effect: "deny",
constraint: {
some: [{ var: "subject.groups" }, { in: [{ var: "" }, ["group5"]] }],
},
export const ContextualGlobStatement: PolicyStatement = {
sid: "ContextualGlobStatement",
action: "*",
effect: "allow",
constraint: {
"===": [{ var: "subject.id" }, { var: "doc.createdBy" }],
},
"ContextualDenyStatement"
);
};

export const InvalidConstraintStatement = parsePolicyStatement(
{
action: "create",
effect: "allow",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constraint: { foo: "bar" } as any,
},
"InvalidConstraintStatement"
);
export const InvalidConstraintStatement: PolicyStatement = {
sid: "InvalidConstraintStatement",
action: Actions.create,
effect: "allow",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constraint: { foo: "bar" } as any,
};
34 changes: 23 additions & 11 deletions lib/parsed-policy-statement.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RulesLogic } from "json-logic-js";
import { randomUUID } from "node:crypto";
import { PolicyStatement } from "./types";
import { arrayify } from "./utils/arr";
import { traverseRulesLogic } from "./utils/logic";
import {
isStringLiteral,
Expand All @@ -14,13 +15,18 @@ export type ActionsByType = {
globAll: boolean;
};

export const SYM_SID = Symbol("PolicyStatementID");
export type ParsedPolicyStatement = {
/** a globally-unique statement identifier */
sid: string;

export type ParsedPolicyStatement = PolicyStatement & {
/** an optional grouping identifier */
gid?: string;

effect: PolicyStatement["effect"];
constraint: PolicyStatement["constraint"];
/** paths referenced in the logic with { var: 'my.path' } */
contextPaths: string[];
actionsByType: ActionsByType;
[SYM_SID]: string;
};

const LIST_OPS = ["map", "reduce", "filter", "all", "none", "some"];
Expand All @@ -40,9 +46,7 @@ function extractVarPaths(logic: RulesLogic): string[] {
}

// syntactic sugar means var could be one of string, [string], or [string, string]
const args = Array.isArray(innerLogic.var)
? innerLogic.var
: [innerLogic.var];
const args = arrayify(innerLogic.var);
if (typeof args[0] !== "string") {
throw new Error(
`var: only path strings are permitted (at ${path.join(".")})`
Expand Down Expand Up @@ -80,17 +84,25 @@ export function parsePolicyActions(actions: string[]): ActionsByType {
return res;
}

interface ParseOptions {
sid?: string;
}

export function parsePolicyStatement(
statement: PolicyStatement,
sid?: string
opts: ParseOptions = {}
): ParsedPolicyStatement {
const { action, constraint } = statement;
const actions = Array.isArray(action) ? action : [action];
const { action, constraint, effect } = statement;
const actions = arrayify(action);

const sid = opts.sid ?? statement.sid ?? randomUUID();

return {
...statement,
sid,

effect,
constraint,
contextPaths: extractVarPaths(constraint),
actionsByType: parsePolicyActions(actions),
[SYM_SID]: sid ?? randomUUID(),
};
}
Loading