|
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 = '[email protected]' |
|
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) |
|
}) |
|
} |
|
} |