-
Notifications
You must be signed in to change notification settings - Fork 825
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add @auth base package with Access Control
- Loading branch information
Showing
29 changed files
with
1,315 additions
and
179 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
**/__mocks__/** | ||
**/__tests__/** | ||
src | ||
tsconfig.json | ||
tsconfig.tsbuildinfo |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
packages/amplify-graphql-auth-transformer/src/__tests__/ac_tests/accesscontrol.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
162
packages/amplify-graphql-auth-transformer/src/accesscontrol/acm.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
packages/amplify-graphql-auth-transformer/src/accesscontrol/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { AccessControlMatrix } from './acm'; |
Oops, something went wrong.