Skip to content

Commit

Permalink
feat: add @auth base package with Access Control
Browse files Browse the repository at this point in the history
  • Loading branch information
SwaySway committed Sep 7, 2021
1 parent 4ee9232 commit 31bd152
Show file tree
Hide file tree
Showing 29 changed files with 1,315 additions and 179 deletions.
5 changes: 5 additions & 0 deletions packages/amplify-graphql-auth-transformer/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**/__mocks__/**
**/__tests__/**
src
tsconfig.json
tsconfig.tsbuildinfo
Empty file.
60 changes: 60 additions & 0 deletions packages/amplify-graphql-auth-transformer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"name": "@aws-amplify/graphql-auth-transformer",
"version": "0.1.0",
"description": "Amplify GraphQL @auth Transformer",
"repository": {
"type": "git",
"url": "https://github.com/aws-amplify/amplify-cli.git",
"directory": "packages/amplify-graphql-auth-transformer"
},
"author": "Amazon Web Services",
"license": "Apache-2.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"keywords": [
"graphql",
"cloudformation",
"aws",
"amplify"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "jest",
"build": "tsc",
"clean": "rimraf ./lib",
"watch": "tsc -w"
},
"dependencies": {
"@aws-amplify/graphql-transformer-core": "0.8.1",
"@aws-amplify/graphql-transformer-interfaces": "1.8.1",
"@aws-amplify/graphql-model-transformer": "0.5.1",
"@aws-cdk/core": "~1.72.0",
"constructs": "^3.0.12",
"graphql": "^14.5.8",
"graphql-mapping-template": "4.18.2",
"graphql-transformer-common": "4.19.7",
"lodash": "^4.17.21"
},
"devDependencies": {
"@types/fs-extra": "^8.0.1",
"@types/node": "^12.12.6"
},
"jest": {
"testURL": "http://localhost",
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(src/__tests__/.*.test.ts)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
"collectCoverage": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { AccessControlMatrix } from '../../accesscontrol';
import { MODEL_OPERATIONS } from '../../utils';

test('test access control', () => {
/*
given the following schema
type Student
@model
@auth(rules: [
{ allow: groups, groups: ["admin"] }
{ allow: groups, groups: ["student"], operations: [read] }
]) {
studentID: ID
name: String
#acm protect email only studentID can update their own email
email: AWSEmail @auth(rules: [
{ allow: owner, ownerField: "studentID", operations: [update] }
{ allow: groups, groups: ["admin"] }
])
# only allowed to student and admin
ssn: String @auth(rules: [
{ allow: owner, ownerField: "studentID", operations: [read] }
{ allow: groups, groups: ["admin"] }
])
}
*/
// create an acm for the student type
const adminRole = 'userPools:staticGroup:admin';
const studentGroupRole = 'userPools:staticGroup:student';
const studentOwnerRole = 'userPools:owner:studentID';
const studentTypeFields = ['studentID', 'name', 'email', 'ssn'];
const acm = new AccessControlMatrix({
resources: studentTypeFields,
operations: MODEL_OPERATIONS,
});
// add OBJECT rules first
// add admin role which has full access on all CRUD operations for all fields
acm.setRole({
role: adminRole,
operations: MODEL_OPERATIONS,
});
// add the student static group rule which only has read access
acm.setRole({
role: studentGroupRole,
operations: ['read'],
});

studentTypeFields.forEach(field => {
// check that admin has CRUD access on all fields
expect(acm.isAllowed(adminRole, field, 'create')).toBe(true);
expect(acm.isAllowed(adminRole, field, 'read')).toBe(true);
expect(acm.isAllowed(adminRole, field, 'update')).toBe(true);
expect(acm.isAllowed(adminRole, field, 'delete')).toBe(true);
// check that studentGroupRole has access to read only
expect(acm.isAllowed(studentGroupRole, field, 'read')).toBe(true);
expect(acm.isAllowed(studentGroupRole, field, 'create')).toBe(false);
expect(acm.isAllowed(studentGroupRole, field, 'update')).toBe(false);
expect(acm.isAllowed(studentGroupRole, field, 'delete')).toBe(false);
});
// when adding a field rule on email we need to overwrite it
acm.resetAccessForResource('email');

expect(acm.isAllowed(studentGroupRole, 'email', 'read')).toBe(false);
acm.setRole({
role: studentOwnerRole,
operations: ['update'],
resource: 'email',
});
expect(acm.isAllowed(adminRole, 'email', 'update')).toBe(false);
expect(acm.isAllowed(studentOwnerRole, 'email', 'update')).toBe(true);
});
162 changes: 162 additions & 0 deletions packages/amplify-graphql-auth-transformer/src/accesscontrol/acm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import assert from 'assert';

type ACMConfig = {
resources: string[];
operations: string[];
};

type SetRoleInput = {
role: string;
operations: Array<string>;
resource?: string;
};

type ValidateInput = {
role?: string;
resource?: string;
operations?: Array<string>;
};

type ResourceOperationInput = {
operations: Array<string>;
role?: string;
resource?: string;
};

/**
* Creates an access control matrix
* The following vectors are used
* - Roles
* - Resources
* - Operations
*/
export class AccessControlMatrix {
private roles: Array<string>;
private operations: Array<string>;
private resources: Array<string>;
private matrix: Array<Array<Array<boolean>>>;

constructor(config: ACMConfig) {
this.operations = config.operations;
this.resources = config.resources;
this.matrix = new Array();
this.roles = new Array();
}

public setRole(input: SetRoleInput): void {
const { role, resource, operations } = input;
this.validate({ resource, operations });
let allowedVector: Array<Array<boolean>>;
if (!this.roles.includes(role)) {
allowedVector = this.getResourceOperationMatrix({ operations, resource });
this.roles.push(input.role);
this.matrix.push(allowedVector);
assert(this.roles.length === this.matrix.length, 'Roles are not aligned with Roles added in Matrix');
} else {
allowedVector = this.getResourceOperationMatrix({ operations, resource, role });
const roleIndex = this.roles.indexOf(role);
this.matrix[roleIndex] = allowedVector;
}
}

public hasRole(role: string): boolean {
return this.roles.includes(role);
}

public getResources(): Array<string> {
return this.resources;
}

public isAllowed(role: string, resource: string, operation: string): boolean {
this.validate({ role, resource, operations: [operation] });
const roleIndex = this.roles.indexOf(role);
const resourceIndex = this.resources.indexOf(resource);
const operationIndex = this.operations.indexOf(operation);
return this.matrix[roleIndex][resourceIndex][operationIndex];
}

public resetAccessForResource(resource: string): void {
this.validate({ resource });
const resourceIndex = this.resources.indexOf(resource);
for (let i = 0; i < this.roles.length; i++) {
this.matrix[i][resourceIndex] = new Array(this.operations.length).fill(false);
}
}

/**
* @returns a map of role and their access
* this object can then be used in console.table()
*/
public getAcmPerRole(): Map<string, Object> {
const acmPerRole: Map<string, Object> = new Map();
for (let i = 0; i < this.matrix.length; i++) {
let tableObj: any = {};
for (let y = 0; y < this.matrix[i].length; y++) {
tableObj[this.resources[y]] = this.matrix[i][y].reduce((prev: any, resource: boolean, index: number) => {
prev[this.operations[index]] = resource;
return prev;
}, {});
}
acmPerRole.set(this.roles[i], tableObj);
}
return acmPerRole;
}

/**
* helpers
*/
private validate(input: ValidateInput) {
if (input.resource && !this.resources.includes(input.resource)) {
throw Error(`Resource: ${input.resource} is not configued in the ACM`);
}
if (input.role && !this.roles.includes(input.role)) {
throw Error(`Role: ${input.role} does not exist in ACM.`);
}
if (input.operations) {
input.operations.forEach(operation => {
if (this.operations.indexOf(operation) === -1) throw Error(`Operation: ${operation} does not exist in the ACM.`);
});
}
}

/**
*
* if singular resource is specified the operations are only applied on the resource
* a denied array for every other resource is returned in the matrix
* @param operations
* @param resource
* @returns a 2d matrix containg the access for each resource
*/
private getResourceOperationMatrix(input: ResourceOperationInput): Array<Array<boolean>> {
const { operations, resource, role } = input;
let fieldAllowVector: boolean[][] = [];
let operationList: boolean[] = this.getOperationList(operations);
if (role && resource) {
const roleIndex = this.roles.indexOf(role);
const resourceIndex = this.resources.indexOf(resource);
fieldAllowVector = this.matrix[roleIndex];
fieldAllowVector[resourceIndex] = operationList;
return fieldAllowVector;
}
for (let i = 0; i < this.resources.length; i++) {
if (resource) {
if (this.resources.indexOf(resource) === i) {
fieldAllowVector.push(operationList);
} else {
fieldAllowVector.push(new Array(this.resources.length).fill(false));
}
} else {
fieldAllowVector.push(operationList);
}
}
return fieldAllowVector;
}

private getOperationList(operations: Array<string>): Array<boolean> {
let operationList: Array<boolean> = new Array();
for (let operation of this.operations) {
operationList.push(operations.includes(operation));
}
return operationList;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AccessControlMatrix } from './acm';
Loading

0 comments on commit 31bd152

Please sign in to comment.