Skip to content

Instantly share code, notes, and snippets.

@Atlinx
Created May 31, 2022 22:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Atlinx/f79309272da119aa091589022973a04e to your computer and use it in GitHub Desktop.
Save Atlinx/f79309272da119aa091589022973a04e to your computer and use it in GitHub Desktop.
Attempt at security middleware for Typetta. Currently doesn't work.
import { Permission } from "@src/generated/model.types";
export type SecurityParams = {
[entity: string]: {
[field: string]: ((newValue: any) => Promise<Error | void>)[]
}
}
export function mergeSecurityParams(... params: SecurityParams[]) {
return mergeUnionArraysMany(...params) as SecurityParams;
}
export function mergeUnionArraysMany(... elements: any[]) {
if (elements.length == 1)
return elements[0];
let finalObject: any = mergeUnionArrays(elements[0], elements[1])
for (let i = 1; i < elements.length - 1; i++) {
finalObject = mergeUnionArrays(elements[i], elements[i + 1]);
}
return finalObject;
}
export function mergeUnionArrays(one: any, two: any) {
const checkedProps = new Set<string>();
return {
...mergeUnionArraysHalf(one, two, checkedProps),
...mergeUnionArraysHalf(two, one, checkedProps)
}
}
export function mergeUnionArraysHalf(one: any, two: any, checkedProps: Set<string>) {
const finalObject: any = {};
for (const oneProp in one) {
if (checkedProps.has(oneProp))
continue;
checkedProps.add(oneProp);
const oneValue = one[oneProp];
const twoValue = two[oneProp];
if (!oneValue)
continue;
if (twoValue) {
if (Array.isArray(oneValue) && Array.isArray(twoValue)) {
finalObject[oneProp] = [ ...oneValue, ...twoValue ];
} else if (typeof twoValue === "object" && typeof oneValue === "object") {
finalObject[oneProp] = mergeUnionArrays(twoValue, oneValue);
} else {
// We are a non mergable type, so do nothing
}
} else {
finalObject[oneProp] = oneValue;
}
}
return finalObject;
}
type KeyIndexable = number | string | symbol;
// Wrapper for types
export type SecurityTypes<TPermissionCodes extends KeyIndexable, TSecurityDomain = ""> = {
securityContext: SecurityContext<TSecurityDomain, TPermissionCodes>
policyPermission: PolicyPermission<TSecurityDomain, TPermissionCodes>
policyBlueprint: PolicyBlueprint<TSecurityDomain, TPermissionCodes>
policyBuilder: PolicyBuilder<TSecurityDomain, TPermissionCodes>
}
type MANAGE_ROLE_CONTEXT = {
}
type UPDATE_USER_CONTEXT = {
userId: string
}
type SEE_USER_CONTEXT = {
userId: string
projectMemberId: string
}
type MySecurityContext = MANAGE_ROLE_CONTEXT | UPDATE_USER_CONTEXT | SEE_USER_CONTEXT
const test: SecurityContext<MySecurityContext, Permission> = {
CREATE_PROJECT: [{
userId: "sdfsdf",
extraData: "extra data!"
}]
}
/**
A SecurityContext respents a specific user's
access to information. The class itself
contains all the permissions the user has.
*/
type SecurityContext<TSecurityDomain, TPermissionCodes extends KeyIndexable> = {
[permission in TPermissionCodes]?: Partial<TSecurityDomain>[]
}
type PolicyPermission<TSecurityDomain, TPermissionCodes extends KeyIndexable> = {
name: keyof TPermissionCodes
domainMapping?: SecurityDomainToEntityFieldMapping<TSecurityDomain>
children?: (keyof TPermissionCodes)[]
operations: Operation<TSecurityDomain, TPermissionCodes>[]
}
class PolicyBlueprint<TSecurityDomain, TPermissionCodes extends KeyIndexable> {
constructor(
public name: string,
public permissions: PolicyPermission<TSecurityDomain, TPermissionCodes>[]
) {}
getPermission(name: keyof TPermissionCodes) {
return this.permissions.find(x => x.name === name);
}
hasPermission(name: keyof TPermissionCodes, context: SecurityContext<TSecurityDomain, TPermissionCodes>) {
const permission = this.getPermission(name);
for (const permissionCode in context) {
const permission = context[permissionCode];
if (permission) {
for (const domain of permission as Partial<TSecurityDomain>[]) {
if (domain)
}
}
}
}
}
type SecurityDomainToEntityFieldMapping<TSecurityDomain> = {
[domainField in keyof TSecurityDomain]: string;
}
type Operation<TSecurityDomain, TPermissionCodes extends KeyIndexable> = {
type: string
/**
Ex. "entity.field.subfield",
"entity.field",
"entity"
*/
entityFieldPath: string
args: any
}
class PolicyBuilder<TSecurityDomain, TPermissionCodes extends KeyIndexable> {
constructor(
public context: SecurityContext<TSecurityDomain, TPermissionCodes>
) {}
policy(name: string, permissions: PolicyPermission<TSecurityDomain, TPermissionCodes>[]) {
return new PolicyBlueprint<TSecurityDomain, TPermissionCodes>(
name,
permissions
);
}
permission(
name: keyof TPermissionCodes,
settings: {
domainMapping?: SecurityDomainToEntityFieldMapping<TSecurityDomain>,
children?: (keyof TPermissionCodes)[]
},
operationArrays: Operation<TSecurityDomain, TPermissionCodes>[][]
) {
return <PolicyPermission<TSecurityDomain, TPermissionCodes>>{
name,
...settings,
// Flatten operations
operations: ([] as Operation<TSecurityDomain, TPermissionCodes>[]).concat(...operationArrays),
}
}
crud(
entityFieldPath: string,
operations: {
create?: boolean | { [field: KeyIndexable]: boolean }
read?: boolean | { [field: KeyIndexable]: boolean }
update?: boolean | { [field: KeyIndexable]: boolean }
delete?: boolean | { [field: KeyIndexable]: boolean }
}
): Operation<TSecurityDomain, TPermissionCodes>[] {
let operationsArr: Operation<TSecurityDomain, TPermissionCodes>[] = [];
if (operations.create)
operationsArr = operationsArr.concat(this.create(entityFieldPath, operations.create));
if (operations.read)
operationsArr = operationsArr.concat(this.read(entityFieldPath, operations.read));
if (operations.update)
operationsArr = operationsArr.concat(this.update(entityFieldPath, operations.update));
if (operations.delete)
operationsArr = operationsArr.concat(this.delete(entityFieldPath, operations.delete));
return operationsArr;
}
operation(
type: string,
entityFieldPath: string,
args: any = {}
): Operation<TSecurityDomain, TPermissionCodes>[] {
return [{
entityFieldPath,
type,
args
}]
}
create(entityFieldPath: string,
fields: boolean | { [field: KeyIndexable]: boolean }
): Operation<TSecurityDomain, TPermissionCodes>[] {
return this.singleCrud("update", entityFieldPath, fields);
}
read(entityFieldPath: string,
fields: boolean | { [field: KeyIndexable]: boolean }
): Operation<TSecurityDomain, TPermissionCodes>[] {
return this.singleCrud("update", entityFieldPath, fields);
}
update(entityFieldPath: string,
fields: boolean | { [field: KeyIndexable]: boolean }
): Operation<TSecurityDomain, TPermissionCodes>[] {
return this.singleCrud("update", entityFieldPath, fields);
}
delete(entityFieldPath: string,
fields: boolean | { [field: KeyIndexable]: boolean }
): Operation<TSecurityDomain, TPermissionCodes>[] {
return this.singleCrud("update", entityFieldPath, fields);
}
singleCrud(
operationType: string,
entityFieldPath: string,
fields: boolean | { [field: KeyIndexable]: boolean }
): Operation<TSecurityDomain, TPermissionCodes>[] {
if (typeof fields === 'boolean') {
const isOperationAllowed = fields;
return [{
entityFieldPath,
type: operationType,
args: isOperationAllowed
}]
}
let operations: Operation<TSecurityDomain, TPermissionCodes>[] = []
for (const field in fields) {
const isOperationAllowed: boolean = fields[field];
operations.push({
entityFieldPath: entityFieldPath + "." + field,
type: operationType,
args: isOperationAllowed
});
}
return operations;
}
}
/**
namespace perm {
funciton read(model) => { resolver: Resolver.read, args: [ model ] };
}
namespace user {
roles: "user.roles";
}
function policy(context): Policy {
const P = new PolicyBuilder(context);
return {
name: "Hello",
permissions: [
{
name: "MANAGE_USERS"
domainMapping: {
userId: "userId"
},
data: {
},
operations: [
P.allow("user"),
P.deny("user"),
P.read("user"),
P.read("user.roles"),
P.read(user.roles),
P.read("user", {
roles: true,
email: false;
}),
P.crud("user", {
// Operations
read: true
update: true
create: true
})
],
},
]
});
}
onlyRolesBelow(conetxt) {
}
findAll(filter: {
})
*/
export function securityMiddleware(...allParams: SecurityParams[]) {
const securityParams = mergeSecurityParams(...allParams);
return {
async before(args: any, context: any) {
if (args.operation === 'read') {
args.params.filter
}
let changes;
switch(args.operation) {
case 'insert':
changes = args.params.record
break;
case 'replace':
changes = args.params.replace
break;
case 'update':
changes = args.params.changes
break;
default:
return {
...args,
continue: true
}
}
// Find the set of validation functions for this object
const validationFns = securityParams[context.daoName];
if (!validationFns) {
return {
...args,
continue: true
}
}
// Apply the validation functions corresopnding to each
// field for the fields that have changed.
for (const key in changes) {
if (!validationFns[key])
continue;
for (const validationFn of validationFns[key]) {
const error = await validationFn(changes[key]);
if (error) {
error.message = `Validation failed on field "${context.daoName}.${key}": ${error.message}`;
throw error;
}
}
}
return {
...args,
continue: true
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment