Skip to content

Instantly share code, notes, and snippets.

@cdelgadob
Last active July 4, 2019 19:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save cdelgadob/7fd2d900ef575257905c2ebe61acf781 to your computer and use it in GitHub Desktop.
Save cdelgadob/7fd2d900ef575257905c2ebe61acf781 to your computer and use it in GitHub Desktop.
This is my authorizer implementation for out graphQL endpoint. This is based on groups, and validates first the operation being called (same as a REST endpoint) and then the query. It has several points for improvement but I wanted to share it with the community. Hope it can serve as an inspiration to somebody looking for this functionality. Ple…
[
{"name": "deleteRestaurantService","roles": ["bus_mngr","rest_mngr"],"item_type": "operation", "op_type": "ORG"},
{"name": "createRestaurantService","roles": ["bus_mngr","rest_mngr"],"item_type": "operation", "op_type": "ORG"},
{"name": "BookRequestDelta.user","roles": ["user","bus_mngr","rest_mngr"],"item_type": "entity"},
{"name": "BookRequest.deltas","roles": ["bus_mngr","rest_mngr","maitre","user","item_type": "entity"},
{"name": "BookRequest.person","roles": ["user","bus_mngr","rest_mngr","maitre","waiter"],"item_type": "entity"}
]
import * as jwt from 'jsonwebtoken'
import {
AuthPerms as AuthPermsDDB,
RestaurantUserRole,
BusinessUserRole,
User } from './data/connectors'
interface JwtAccessToken {
header?:
{ typ: string,
kid: string,
alg: string }
payload:
{ name: string,
nickname: string,
picture?: string,
updated_at?: string,
sub?: string,
iss?: string,
aud?: string,
exp?: number,
iat?: number}
}
const PUBLIC_ACCESS_ROLE = 'public'
const SUPER_USER_ROLE = 'chefto_mngr'
const CHEFTO_SVC_ROLE = 'chefto_svc'
const CHEFTO_SVC_USER = 'service@cheftonic.com'
const USER_ROLE_LITERAL = 'user'
const BUS_MNGR_ROLE_LITERAL = 'bus_mngr'
const ITEM_TYPE_OPERATION = 'operation'
const OP_NAME_CREATE_BUSINESS = ['createBusiness','createBusinessAndRestaurants']
const OP_TYPE_ORG = 'ORG'
const ARG_ID_SUFFIX = '_id'
const ARG_ID_BUSINESS_ID = 'b_id'
const ARG_ID_RESTAURANT_ID = 'b_r_id'
const ARG_ID_EMAIL = 'email'
const ARG_ID_PERSON_ID = 'p_id'
export class UserPerms {
business_perms?: Map<String, Set<String>>; // [business1,perms1], [business2,perms2],...]
restaurant_perms?: Map<String, Set<String>> ;// [restaurant1,perms1], [restaurant2,perms2],...]
private cheftonicManagerRole:string = SUPER_USER_ROLE
private cheftonicServiceRole:string = CHEFTO_SVC_ROLE
private isCheftoAdmin:boolean = false
private isCheftoSvc:boolean = false
constructor (b_perms, r_perms) {
this.business_perms = new Map<String, Set<String>>(
b_perms.Items.map(business => {
//console.log ("********* business [" + business.attrs.b_id + "] : [" + business.attrs.roles.constructor.name + "] " + business.attrs.roles)
return [business.attrs.b_id,
// Pass the Array iterator as the argument to the Map constructor
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/@@iterator
new Set<String>(business.attrs.roles[Symbol.iterator]())
]
})
)
this.restaurant_perms = new Map<string, Set<String>>(
r_perms.Items.map(restaurant => {
//console.log ("********* restaurant [" + restaurant.attrs.b_r_id + "] : [" + restaurant.attrs.roles.constructor.name + "] " + restaurant.attrs.roles)
// Pass the Array iterator as the argument to the Map constructor
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/@@iterator
return [restaurant.attrs.b_r_id,
new Set<String>(restaurant.attrs.roles[Symbol.iterator]())
]
})
)
// Check if it's cheftonic Admin
if (this.business_perms.get('*')) {
this.isCheftoAdmin = this.business_perms.get('*').has(this.cheftonicManagerRole)
this.isCheftoSvc = this.business_perms.get('*').has(this.cheftonicServiceRole)
}
}
public isCheftonicAdmin():boolean {
return this.isCheftoAdmin
}
public isCheftoService():boolean {
return this.isCheftoSvc
}
}
export class AuthPerms {
name: string;
op_type?: string;
item_type: string;
roles: Set<String>;
constructor (data:any) {
this.name = data.name
this.op_type = data.op_type?data.op_type:null
this.item_type = data.item_type?data.item_type:null
//console.log ("********* AuthPerms roles: " + JSON.stringify(data.roles))
this.roles = new Set<String>(data.roles[Symbol.iterator]())
/*console.log ("********* AuthPerms created with this.roles: ")
for (let x of this.roles) {
console.log(x);
}*/
}
}
export function createQueryAuthorizer (token?: string, isServiceAction?: boolean):Promise<QueryAuthorizer> {
let obj = new QueryAuthorizer(token = token, isServiceAction = isServiceAction);
return obj._init().then((res) => {
// resolve with the object itself
return obj
}).catch ((err:Error) => {
console.error (err)
throw err
})
}
export class QueryAuthorizer {
realUserPerms: UserPerms;
username: string;
authToken: string;
authPerms: Map<String,AuthPerms>;
decodedToken: JwtAccessToken;
effectiveRoles: Set<String>;
isCheftoService: boolean = false;
effectiveUserPerms:UserPerms
constructor (token?: string, isServiceAction:boolean = false) {
this.authToken = token
this.effectiveRoles = (isServiceAction) ? new Set<String>(CHEFTO_SVC_ROLE) : new Set<String>()
this.isCheftoService = isServiceAction
}
_init () {
return (this.isCheftoService ? Promise.resolve(CHEFTO_SVC_USER) : this.validateTokenAndGetUsername (this.authToken))
.then (username => {
return this.loadUserAuthData(username)
}).catch (err => {
console.error ("ERROR - initializing Authorizer: " + err)
throw new Error ("ERROR - initializing Authorizer: " + err)
})
}
private loadUserAuthData(username:string) {
this.username = username
return Promise.all ([
AuthPermsDDB.scan().execAsync(),
this.getBusinessUserRoles (),
this.getRestaurantUserRoles ()
])
.then ( promRes => {
this.authPerms = new Map<String,AuthPerms>(
promRes[0].Items.map((element,idx,arr) => [element.attrs.name, new AuthPerms (element.attrs)]
)
)
this.effectiveUserPerms =
this.realUserPerms = new UserPerms (
promRes[1],
promRes[2]
)
return Promise.resolve(true)
})
}
private loadUserImpersonationPerms (username:string) {
this.username = username
return Promise.all ([
this.getBusinessUserRoles (),
this.getRestaurantUserRoles ()
])
.then ( promRes => {
this.effectiveUserPerms = new UserPerms (
promRes[0],
promRes[1]
)
return Promise.resolve(true)
})
}
private unLoadUserImpersonationPerms () {
this.username = CHEFTO_SVC_USER
this.effectiveUserPerms = this.realUserPerms
return true
}
public impersonateUser (username: string) {
// Only a service can impersonate another user
return (this.isCheftoService) ? this.loadUserImpersonationPerms (username) : Promise.resolve (false)
}
public getServiceRoleBack() {
// Get the service role back, but only if it is marked already as a service
return (this.isCheftoService) ? this.unLoadUserImpersonationPerms () : false
}
/**
* Performs token validation according to Auth0 guidelines:
* https://auth0.com/docs/api-auth/tutorials/verify-access-token
* @param token
*/
private validateTokenAndGetUsername (token: string): Promise<string> {
return new Promise ((resolve, reject) => {
// TODO: Validate token, and reject if it's not valid
let decodedToken
console.log('____________________***************** Encoded JWT:', token)
if (token !== null) {
decodedToken = <JwtAccessToken> jwt.decode (token, {complete: true})
console.log('____________________***************** Decoded JWT:', decodedToken)
} else {
decodedToken = (<JwtAccessToken> {
payload:
{ name : 'anon_user',
nickname: 'anon_user'
}
})
}
this.decodedToken = decodedToken
resolve (decodedToken.payload.name)
})
}
private getBusinessUserRoles () {
console.log ('_________ getBusinessUserRoles called: ' + this.username)
return BusinessUserRole
.query (this.username)
.ascending()
.loadAll()
.execAsync()
}
private getRestaurantUserRoles () {
console.log ('_________ getRestaurantUserRoles called: ' + this.username)
return RestaurantUserRole
.query (this.username)
.ascending()
.loadAll()
.execAsync()
}
private bypassAuth = () => {
// Depends on the environment variable set in serverless.yml. If it's not set it defaults to false
const bypassFromEnvVar = (typeof process.env.bypassAuth !== 'undefined')? process.env.bypassAuth : false
const bypassFromCheftoAdmin = this.effectiveUserPerms.isCheftonicAdmin()
//const bypassFromCheftoSvc = this.userPerms.isCheftoService()
// Bypass authorization if any of the two vars is true
return bypassFromEnvVar || bypassFromCheftoAdmin //|| bypassFromCheftoSvc
}
public authFieldAccess (info:any): Promise<boolean> {
const fieldName = info.parentType + "." + info.fieldName
if (this.bypassAuth()) {
console.log ('___________ Bypassing Auth for ' + fieldName)
}
return this.bypassAuth() ? Promise.resolve(true) : this.authAccess (fieldName)
.then (authRes => {
console.log ('___________ FieldAuth for ' + fieldName + ' - result: ' + authRes)
if (! authRes) {
throw 'Not Authorized'
} else {
return authRes
}
})
.catch (err => {
console.error ('___________ FieldAuth for ' + fieldName + ' - rejected: ', err)
throw 'Not Authorized'
})
}
public authOpAccess (info: any): Promise<boolean> {
const fieldName = info.fieldName
const function_args = info.fieldNodes[0].arguments
const variable_values = info.variableValues
if (this.bypassAuth()) {
console.log ('___________ Bypassing Auth for ' + fieldName)
}
return this.bypassAuth() ? Promise.resolve(true) : this.authAccess (fieldName, function_args, variable_values)
.then ( authRes => {
console.log ('___________ OpAuth for ' + fieldName + ' whith args: ' + function_args + ' - result: ' + authRes)
if (! authRes) {
throw 'Not Authorized'
} else {
return authRes
}
})
.catch (err => {
console.error ('___________ OpAuth for ' + fieldName + ' - rejected: ', err)
throw 'Not Authorized'
})
}
private authAccess (fieldName: string, function_args?:any, variableValues?:any): Promise<boolean> {
return new Promise ((resolve, reject) => {
console.log ('________________ authAccess called for: ', fieldName)
//console.log ('________________ this.authPerms type: ', this.authPerms.get(fieldName).constructor.name)
const fieldAuthInfo:AuthPerms = this.authPerms.get(fieldName)
console.log ("******* fieldAuthInfo.roles: ")
for (let x of fieldAuthInfo.roles) {
console.log(x);
}
//console.log ('________________ fieldAuthInfo: ', JSON.stringify(fieldAuthInfo))
//console.log ('________________ with args: ', JSON.stringify(function_args))
//console.log ('________________ this.authPerms entry: ', fieldAuthInfo)
/*
Doc: https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/filter
Doc strings: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
*/
/*let queryData = function_args
let queryResult = queryData.filter(elem => {
return ((elem.name.value===ARG_ID_BUSINESS_ID)||(elem.name.value===ARG_ID_RESTAURANT_ID))})
console.log ('________________ Query data: ', JSON.stringify(queryData))
console.log ('________________ ID param found: ', JSON.stringify(queryResult))
console.log ('________________ with value: ',queryResult[0].value.value)
resolve (true)
*/
if (fieldAuthInfo.roles.has(PUBLIC_ACCESS_ROLE)) {
// ** Branch 1 **
// Before resolving, check if it's an organizational operation and in that case set the effective roles
if (fieldAuthInfo.item_type === ITEM_TYPE_OPERATION) {
if (fieldAuthInfo.op_type === OP_TYPE_ORG) {
// Duplicate of branch 6
let orgId = this.findOrgId (variableValues)[0]
if (orgId.name === ARG_ID_BUSINESS_ID) {
// ** Branch 10 **
const b_id:string = orgId.value
this.effectiveRoles = this.effectiveUserPerms.business_perms.get(b_id)
} else {
// ** Branch 11 **
const b_r_id:string = orgId.value
this.effectiveRoles = this.effectiveUserPerms.restaurant_perms.get(b_r_id)
}
} else {
// Assign effective role as the user itself
this.effectiveRoles = new Set<String>([USER_ROLE_LITERAL])
}
resolve (true)
} else {
// The item is a field with public access - GRANT
console.log ('*** Field access granted')
resolve(true)
}
} else {
// ** Branch 2 **
if (fieldAuthInfo.item_type === ITEM_TYPE_OPERATION) {
// ** Branch 4 **
// Operation access
if (OP_NAME_CREATE_BUSINESS.indexOf (fieldAuthInfo.name) >= 0) {
// ** Branch 14 **
// SPECIAL CASE - Create business operation
// Check if the user is registered
this.effectiveRoles = new Set<string>([BUS_MNGR_ROLE_LITERAL])
return (!this.username) ? resolve (false) : User.getAsync({email: this.username},{ ProjectionExpression : 'email'})
.then (user => {
// ** Branch 15 **
// ALLOW or DENY depending if the user result exists
resolve (user.attrs?true:false)
}).catch (err => {
// ** Branch 16 **
// User not registered
console.log ('%% ERROR - Validating access for createBusiness. Could not get user info: ', err)
resolve (false)
})
} else {
console.log ('Branch 5: ')
// ** Branch 5 **
// Operation type, check if it's org or user type
if (fieldAuthInfo.op_type === OP_TYPE_ORG) {
console.log ('Branch 6: '+fieldAuthInfo.op_type)
// ** Branch 6 **
// Organizational operation type
// Find the organization id
let orgId = this.findOrgId (variableValues)[0]
if (orgId.name === ARG_ID_BUSINESS_ID) {
// ** Branch 10 **
const b_id:string = orgId.value
this.effectiveRoles = this.effectiveUserPerms.business_perms.get(b_id)
} else {
// ** Branch 11 **
const b_r_id:string = orgId.value
this.effectiveRoles = this.effectiveUserPerms.restaurant_perms.get(b_r_id)
}
// ** Branch 10-11 merge **
// var intersection = new Set([...set1].filter(x => set2.has(x)));
const rolesIntersection = new Set<String>([...fieldAuthInfo.roles].filter(role => this.effectiveRoles.has(role)))
// ** Branches 12-13 **
resolve (rolesIntersection.size > 0)
} else {
// ** Branch 7 **
// User operation type - check if the u_id (email) comes as an argument or use the username. If comes as a variable, it has to match the username
let u_id = this.username
try {
let key_u_id = function_args.filter(elem => {return elem.name.value === ARG_ID_EMAIL}).pop().name.value
u_id = variableValues[key_u_id]
} catch (e){
// do nothing, u_id is the username
}
this.effectiveRoles = new Set<String>([USER_ROLE_LITERAL])
// ** Branches 8-9 **
resolve (u_id === this.username)
}
}
} else {
// ** Branch 3 **
// Field access, using effective user roles
console.log ("******* fieldAuthInfo.roles: ")
for (let x of fieldAuthInfo.roles) {
console.log(x);
}
console.log ("******* this.effectiveRoles: ")
for (let x of this.effectiveRoles) {
console.log(x);
}
let allowedEntityRoles = new Set<String>([...fieldAuthInfo.roles].filter(role => this.effectiveRoles.has(role)))
console.log ("******* allowedEntityRoles: ")
for (let x of allowedEntityRoles) {
console.log(x);
}
// ** Branches 17-18 **
resolve (allowedEntityRoles.size > 0)
}
}
})
}
private findOrgId (objectToFind:object) {
let keysToFind = Object.keys(objectToFind)
return keysToFind.map (key => {
if ((key===ARG_ID_BUSINESS_ID)||(key===ARG_ID_RESTAURANT_ID)) {
return [{name: key, value: objectToFind[key]}]
} else if (typeof objectToFind[key] == "object" && objectToFind[key]) {
return this.findOrgId (objectToFind[key])
} else {
return []
}
}).reduce((prev, curr) => {
return prev.concat(curr)
})
}
}
import {Person as PersonModel} from '../models'
import { QueryAuthorizer } from '../../authorizer'
/*
This is an example of a customer resolver, please note the lines that perfomr the authorization
*/
export const Customer = {
person (customer, args, context, info) {
return (<QueryAuthorizer> context.authorizer).authFieldAccess (info)
.then(authRes => {
return PersonModel.findById (customer.p_id).then ((person) => {
return person
}).catch ((err) => {
console.log('Error getting person in Customer resolver: ', err)
return err
})
})
.catch (err => {
return err
})
}
}
/*
This is the handler, where the query arrives. It is deployed as a lambda function in AWS.
Please note the Token retrieval and the creation/initialization of the query authorizer
*/
module.exports.chftqry = (event, context, callback) => {
let tokenProvided:string
// This is to prevent lower case header - some browsers seem to lower case the header name
if (event.headers.authorization) {
event.headers.Authorization = event.headers.authorization
}
if (event.headers.Authorization) {
// There's and auth header, let's put it inside the context to let graphQL's validator auth the query
tokenProvided = event.headers.Authorization.replace('Bearer ', '')
} else {
tokenProvided = null
}
createQueryAuthorizer(tokenProvided)
.then(obj => {
context.authorizer = obj
//console.log ('_________________ authorizer object created: ', JSON.stringify(context.authorizer))
// 1. Build the Options Object
try {
const cheftonicSchema = makeExecutableSchema({
typeDefs: schema,
resolvers: resolvers,
logger: { log: (e) => console.log(e) }
})
const graphqlServerOpts:GraphQLOptions = {
context: context,
schema: cheftonicSchema,
logFunction: cheftonicLog,
// formatResponse: formatResponseFunction,
formatError: (err) => { console.error(err.stack); return err }
}
let apolloHandler = graphqlLambda (graphqlServerOpts)
const corsHandler = function(event, context, callback) {
const callbackFilter = function(error, output) {
output.headers['Access-Control-Allow-Origin'] = '*';
callback(error, output);
}
return apolloHandler(event, context, callbackFilter);
};
return corsHandler (event, context, callback)
} catch (error) {
console.log ('makeExecutableSchema Error: ', error)
callback (error, null)
}
}).catch(err => {
// error here
console.error ("%%%%%%%%%%%%%% HANDLER ERROR: " + err)
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment