@opuscapita/bouncer
Advanced tools
Comparing version 1.2.5 to 1.2.6
942
index.js
@@ -10,10 +10,10 @@ const extend = require('extend'); | ||
const actionsMap = { | ||
'POST' : 'create', | ||
'GET' : 'view', | ||
'PUT' : 'edit', | ||
'DELETE' : 'delete', | ||
'HEAD' : 'head', | ||
'OPTIONS' : 'options', | ||
'PATCH' : 'patch' | ||
} | ||
'POST': 'create', | ||
'GET': 'view', | ||
'PUT': 'edit', | ||
'DELETE': 'delete', | ||
'HEAD': 'head', | ||
'OPTIONS': 'options', | ||
'PATCH': 'patch' | ||
}; | ||
@@ -31,52 +31,45 @@ /** | ||
*/ | ||
class Bouncer | ||
{ | ||
/** | ||
class Bouncer { | ||
/** | ||
* Initializes a new Bouncer instance and loads all permission configurations. | ||
* @param {object} config - Optional configuration object extending {@link Bouncer.DefaultConfig}. | ||
*/ | ||
constructor(config) | ||
{ | ||
this.config = extend(true, { }, Bouncer.DefaultConfig, config); | ||
this.cache = new Cache({ | ||
driver : 'memory' | ||
}); | ||
} | ||
constructor(config) { | ||
this.config = extend(true, { }, Bouncer.DefaultConfig, config); | ||
this.cache = new Cache({ | ||
driver: 'memory' | ||
}); | ||
} | ||
loadPermissions(src) | ||
{ | ||
if(typeof src === 'string') | ||
return this.normalizePermissions(require(src)); | ||
else if(Array.isArray(src)) | ||
return src.reduce((all, item) => extend(true, all, this.loadPermissions(item)), { }); | ||
else | ||
return this.normalizePermissions(extend(true, { }, src)); | ||
loadPermissions(src) { | ||
if (typeof src === 'string') { | ||
return this.normalizePermissions(require(src)); | ||
} else if (Array.isArray(src)) { | ||
return src.reduce((all, item) => extend(true, all, this.loadPermissions(item)), { }); | ||
} else { | ||
return this.normalizePermissions(extend(true, { }, src)); | ||
} | ||
} | ||
normalizePermissions(permissions) | ||
{ | ||
for(const key in permissions) | ||
{ | ||
const resources = permissions[key].resources; | ||
normalizePermissions(permissions) { | ||
for (const key in permissions) { | ||
const resources = permissions[key].resources; | ||
if(resources && Array.isArray(resources)) | ||
{ | ||
resources.forEach(resource => | ||
{ | ||
if(resource.fields) | ||
{ | ||
resource.requestFields = { allow : resource.fields || null, remove : null }; | ||
delete resource.fields; | ||
} | ||
if (resources && Array.isArray(resources)) { | ||
resources.forEach(resource => { | ||
if (resource.fields) { | ||
resource.requestFields = {allow: resource.fields || null, remove: null}; | ||
delete resource.fields; | ||
} | ||
resource.requestFields = extend(true, { allow : null, remove : null }, resource.requestFields); | ||
resource.responseFields = extend(true, { allow : null, remove : null }, resource.responseFields); | ||
}); | ||
} | ||
} | ||
return permissions; | ||
resource.requestFields = extend(true, {allow: null, remove: null}, resource.requestFields); | ||
resource.responseFields = extend(true, {allow: null, remove: null}, resource.responseFields); | ||
}); | ||
} | ||
} | ||
/** | ||
return permissions; | ||
} | ||
/** | ||
* Calling this method triggers the **bouncer.permissionsReady** event using RabbitMQ for | ||
@@ -89,39 +82,34 @@ * publishing the permissions loaded by the current Bouncer instance. | ||
*/ | ||
async registerPermissions({ retryTimeout = 1000, retryCount = 30 } = { }) | ||
{ | ||
const eventClient = new EventClient({ exchangeName : 'bouncer', logger : this.config.logger }); | ||
const permissions = this.loadPermissions(this.config.permissions); | ||
async registerPermissions({retryTimeout = 1000, retryCount = 30} = { }) { | ||
const eventClient = new EventClient({exchangeName: 'bouncer', logger: this.config.logger}); | ||
const permissions = this.loadPermissions(this.config.permissions); | ||
let retryCounter = 0; | ||
let retryCounter = 0; | ||
do | ||
{ | ||
try | ||
{ | ||
if(await eventClient.queueExists('acl/bouncer.permissionsReady')) | ||
{ | ||
await eventClient.emit('bouncer.permissionsReady', { serviceName : this.config.serviceName, permissions : permissions }); | ||
break; | ||
} | ||
else | ||
{ | ||
throw new Error('Required queue "acl/bouncer.permissionsReady" does not exist.'); | ||
} | ||
} | ||
catch(e) | ||
{ | ||
retryCounter++; | ||
do { | ||
try { | ||
if (await eventClient.queueExists('acl/bouncer.permissionsReady')) { | ||
await eventClient.emit( | ||
'bouncer.permissionsReady', {serviceName: this.config.serviceName, permissions: permissions} | ||
); | ||
break; | ||
} else { | ||
throw new Error('Required queue "acl/bouncer.permissionsReady" does not exist.'); | ||
} | ||
} catch (e) { | ||
retryCounter++; | ||
if(retryCounter < retryCount) | ||
await new Promise(resolve => setTimeout(resolve, retryTimeout)); | ||
else | ||
throw e; | ||
} | ||
if (retryCounter < retryCount) { | ||
await new Promise(resolve => setTimeout(resolve, retryTimeout)); | ||
} else { | ||
throw e; | ||
} | ||
while(retryCounter < retryCount); | ||
await eventClient.dispose(); | ||
} | ||
} | ||
while (retryCounter < retryCount); | ||
/** | ||
await eventClient.dispose(); | ||
} | ||
/** | ||
* Tries to find a resource matching a certain URL based on the request's HTTP verb and the user requesting it. | ||
@@ -135,89 +123,83 @@ * @param {string} url - Path of the requested URL. | ||
*/ | ||
async findResources(url, method, userData, serviceClient = null, serviceName = null) | ||
{ | ||
const roles = (userData && userData.roles) || [ ]; | ||
async findResources(url, method, userData, serviceClient = null, serviceName = null) { | ||
const roles = (userData && userData.roles) || []; | ||
const _serviceName = serviceName || this.config.serviceName; | ||
const _serviceName = serviceName || this.config.serviceName; | ||
const userId = userData && userData.id; | ||
const cacheKey = `${_serviceName}${url}:${method}:${userId}`; | ||
const userId = userData && userData.id; | ||
const cacheKey = `${_serviceName}${url}:${method}:${userId}`; | ||
let foundResources = await this.cache.get(cacheKey); | ||
let foundResources = await this.cache.get(cacheKey); | ||
if(!foundResources) | ||
{ | ||
foundResources = [ ]; | ||
if (!foundResources) { | ||
foundResources = []; | ||
const prefixLength = _serviceName.length + 1; | ||
const action = actionsMap[method.toUpperCase()]; | ||
const prefixLength = _serviceName.length + 1; | ||
const action = actionsMap[method.toUpperCase()]; | ||
if(!serviceClient) | ||
serviceClient = new ServiceClient({ logger : this.config.logger }); | ||
if (!serviceClient) { | ||
serviceClient = new ServiceClient({logger: this.config.logger}); | ||
} | ||
const [ permissions, resourceGroups ] = await Promise.all([ | ||
this.getPermissions(roles, serviceClient, _serviceName), | ||
this.getResourceGroups(serviceClient, _serviceName) | ||
]); | ||
const [permissions, resourceGroups] = await Promise.all([ | ||
this.getPermissions(roles, serviceClient, _serviceName), | ||
this.getResourceGroups(serviceClient, _serviceName) | ||
]); | ||
for(const permission of permissions) | ||
{ | ||
const resourceGroupName = permission.resourceGroupId.substring(prefixLength); | ||
for (const permission of permissions) { | ||
const resourceGroupName = permission.resourceGroupId.substring(prefixLength); | ||
if(resourceGroupName === '*' || this.checkAlwaysAllow([ permission.role ])) | ||
{ | ||
foundResources.push({ | ||
type: [ "rest", "ui" ], | ||
resourceId: '^/', | ||
actions: [ 'create', 'view', 'edit', 'delete', 'head', 'options', 'patch' ], | ||
requestFields: { allow: null, remove: null }, | ||
responseFields: { allow: null, remove: null }, | ||
roleIds : [ permission.role ] | ||
}); | ||
} | ||
else | ||
{ | ||
const foundResourceGroup = resourceGroups[resourceGroupName]; | ||
const resources = foundResourceGroup && foundResourceGroup.resources; | ||
if (resourceGroupName === '*' || this.checkAlwaysAllow([permission.role])) { | ||
foundResources.push({ | ||
type: ['rest', 'ui'], | ||
resourceId: '^/', | ||
actions: ['create', 'view', 'edit', 'delete', 'head', 'options', 'patch'], | ||
requestFields: {allow: null, remove: null}, | ||
responseFields: {allow: null, remove: null}, | ||
roleIds: [permission.role] | ||
}); | ||
} else { | ||
const foundResourceGroup = resourceGroups[resourceGroupName]; | ||
const resources = foundResourceGroup && foundResourceGroup.resources; | ||
if(resources) | ||
{ | ||
const filtered = resources.filter(r => url.match(new RegExp(this.replacePlaceholders(r.resourceId, userData), 'i')) && r.actions.indexOf(action) > -1) | ||
.map(r => ({ ...r, roleIds : [ permission.role ] })); | ||
if (resources) { | ||
const filtered = resources. | ||
filter(r => | ||
url.match(new RegExp(this.replacePlaceholders(r.resourceId, userData), 'i')) && | ||
r.actions.indexOf(action) > -1). | ||
map(r => ({...r, roleIds: [permission.role]})); | ||
foundResources = foundResources.concat(filtered); | ||
} | ||
} | ||
} | ||
foundResources.length && this.cache.put(cacheKey, foundResources, 600); | ||
foundResources = foundResources.concat(filtered); | ||
} | ||
} | ||
} | ||
return foundResources || [ ]; | ||
foundResources.length && this.cache.put(cacheKey, foundResources, 600); | ||
} | ||
async getResourceGroups(serviceClient, serviceName) | ||
{ | ||
const url = `/api/resourceGroups/${serviceName}?type=rest`; | ||
const cacheKey = this.config.aclServiceName + ':' + url; | ||
const cached = await this.cache.get(cacheKey); | ||
return foundResources || []; | ||
} | ||
return cached || serviceClient.get(this.config.aclServiceName, url).then(([ results ]) => | ||
{ | ||
const resourceGroups = { }; | ||
results.forEach(result => resourceGroups[result.resourceGroupId] = result); | ||
async getResourceGroups(serviceClient, serviceName) { | ||
const url = `/api/resourceGroups/${serviceName}?type=rest`; | ||
const cacheKey = `${this.config.aclServiceName}:${url}`; | ||
const cached = await this.cache.get(cacheKey); | ||
this.cache.put(cacheKey, resourceGroups, 600); | ||
return cached || serviceClient.get(this.config.aclServiceName, url).then(([results]) => { | ||
const resourceGroups = { }; | ||
results.forEach(result => resourceGroups[result.resourceGroupId] = result); | ||
return resourceGroups; | ||
}) | ||
.catch(e => | ||
{ | ||
const message = (e.response && e.response.result && e.response.result.message) || e.message; | ||
const statusCode = (e.response && e.response.statusCode) || '-'; | ||
this.cache.put(cacheKey, resourceGroups, 600); | ||
throw new Error(`Could not get resource groups from acl service: "${message}" (${statusCode})`); | ||
}); | ||
} | ||
return resourceGroups; | ||
}). | ||
catch(e => { | ||
const message = (e.response && e.response.result && e.response.result.message) || e.message; | ||
const statusCode = (e.response && e.response.statusCode) || '-'; | ||
/** | ||
throw new Error(`Could not get resource groups from acl service: "${message}" (${statusCode})`); | ||
}); | ||
} | ||
/** | ||
* Gets a list of all business partner identifiers a user is assigned to for a given REST resource. | ||
@@ -238,16 +220,14 @@ * | ||
*/ | ||
async getUserBusinessPartnerIdsByUrl(url, userData, serviceClient = null, method = 'GET', serviceName = null) | ||
{ | ||
const resources = await this.findResources(url, method, userData, serviceClient, serviceName); | ||
async getUserBusinessPartnerIdsByUrl(url, userData, serviceClient = null, method = 'GET', serviceName = null) { | ||
const resources = await this.findResources(url, method, userData, serviceClient, serviceName); | ||
if(resources.length > 0) | ||
{ | ||
const resource = await this.mergeResources(resources); | ||
return (resource && this.getUserBusinessPartnerIds(userData, resource.roleIds)) || [ ]; | ||
} | ||
return [ ]; | ||
if (resources.length > 0) { | ||
const resource = await this.mergeResources(resources); | ||
return (resource && this.getUserBusinessPartnerIds(userData, resource.roleIds)) || []; | ||
} | ||
/** | ||
return []; | ||
} | ||
/** | ||
* Gets a list of all business partner identifiers a user role is assigned to. | ||
@@ -265,28 +245,31 @@ * | ||
*/ | ||
async getUserBusinessPartnerIds(userData, roleIds) | ||
{ | ||
let result; | ||
async getUserBusinessPartnerIds(userData, roleIds) { | ||
let result; | ||
if(userData) | ||
{ | ||
const roleConstraints = roleIds && Array.isArray(userData.xroles) && userData.xroles.filter(r => roleIds.includes(r.role)).map(r => r.businessPartners); | ||
if (userData) { | ||
const roleConstraints = roleIds && | ||
Array.isArray(userData.xroles) && | ||
userData.xroles.filter(r => roleIds.includes(r.role)).map(r => r.businessPartners); | ||
if(userData.roles && userData.roles.indexOf('admin') > -1) | ||
result = [ '*' ]; | ||
else if(userData.id && userData.id.match(/^svc_[^@]*$/)) | ||
result = [ '*' ]; | ||
else if(Array.isArray(roleConstraints) && roleConstraints.length > 0) | ||
result = this.mergeRoleConstraints(roleConstraints); | ||
else if(userData.supplierid) // TODO: Remove this after full migration to Business Partner | ||
result = [ userData.supplierid ]; | ||
else if(userData.customerid) // TODO: Remove this after full migration to Business Partner | ||
result = [ userData.customerid ]; | ||
else if(userData.businesspartner && userData.businesspartner.id) | ||
result = [ userData.businesspartner.id ]; | ||
} | ||
return result || [ ]; | ||
if (userData.roles && userData.roles.indexOf('admin') > -1) { | ||
result = ['*']; | ||
} else if (userData.id && userData.id.match(/^svc_[^@]*$/)) { | ||
result = ['*']; | ||
} else if (Array.isArray(roleConstraints) && roleConstraints.length > 0) { | ||
result = this.mergeRoleConstraints(roleConstraints); | ||
} else if (userData.supplierid) { | ||
// TODO: Remove this after full migration to Business Partner | ||
result = [userData.supplierid]; | ||
} else if (userData.customerid) { | ||
// TODO: Remove this after full migration to Business Partner | ||
result = [userData.customerid]; | ||
} else if (userData.businesspartner && userData.businesspartner.id) { | ||
result = [userData.businesspartner.id]; | ||
} | ||
} | ||
/** | ||
return result || []; | ||
} | ||
/** | ||
* | ||
@@ -299,15 +282,15 @@ * @param {string} serviceName - Service name for identifying a resource group identifier. | ||
*/ | ||
async getUsersByPermissionAndBusinessPartner(serviceName, resourceGroupId, businessPartnerId, serviceClient = null) | ||
{ | ||
if(!serviceClient) | ||
serviceClient = new ServiceClient({ logger : this.config.logger }); | ||
async getUsersByPermissionAndBusinessPartner(serviceName, resourceGroupId, businessPartnerId, serviceClient = null) { | ||
if (!serviceClient) { | ||
serviceClient = new ServiceClient({logger: this.config.logger}); | ||
} | ||
const [ permissions ] = await serviceClient.get('acl', `/api/resourcePermissions/${serviceName}/${resourceGroupId}`, true); | ||
const roles = permissions.map(p => p.role); | ||
const [permissions] = await serviceClient.get('acl', `/api/resourcePermissions/${serviceName}/${resourceGroupId}`, true); | ||
const roles = permissions.map(p => p.role); | ||
const businessPartners = new Set([ businessPartnerId ]); | ||
const businessPartners = new Set([businessPartnerId]); | ||
const [ businessPartner ] = await serviceClient.get('business-partner', `/api/business-partners/${businessPartnerId}`, true); | ||
const [businessPartner] = await serviceClient.get('business-partner', `/api/business-partners/${businessPartnerId}`, true); | ||
/** | ||
/** | ||
* !Attention: Adding the hierarchy notation with "customerId/*" instead of the actual tenantId | ||
@@ -318,11 +301,12 @@ * to the list of tenants hacks the query in the user service to not return exact tenant | ||
*/ | ||
if(businessPartner && businessPartner.hierarchyId) | ||
businessPartner.hierarchyId.split('|').forEach(bp => businessPartners.add(`${bp}/*`)); | ||
if (businessPartner && businessPartner.hierarchyId) { | ||
businessPartner.hierarchyId.split('|').forEach(bp => businessPartners.add(`${bp}/*`)); | ||
} | ||
const [ users ] = await serviceClient.get('user', `/api/users?include=profile&roles=${roles.join(',')}&businessPartners=${Array.from(businessPartners).join(',')}`, true); | ||
const [users] = await serviceClient.get('user', `/api/users?include=profile&roles=${roles.join(',')}&businessPartners=${Array.from(businessPartners).join(',')}`, true); | ||
return users; | ||
} | ||
return users; | ||
} | ||
/** | ||
/** | ||
* | ||
@@ -336,25 +320,24 @@ * @param {string} serviceName - Service name for identifying a resource group identifier. | ||
*/ | ||
async getUsersByPermissionAndTenant(serviceName, resourceGroupId, tenantId, serviceClient = null) | ||
{ | ||
if(!serviceClient) | ||
serviceClient = new ServiceClient({ logger : this.config.logger }); | ||
// TODO: use default logger (it should inherit context from parent call) | ||
async getUsersByPermissionAndTenant(serviceName, resourceGroupId, tenantId, serviceClient = null) { | ||
if (!serviceClient) { | ||
serviceClient = new ServiceClient({logger: this.config.logger}); | ||
} | ||
// TODO: use default logger (it should inherit context from parent call) | ||
const [ permissions ] = await serviceClient.get('acl', `/api/resourcePermissions/${serviceName}/${resourceGroupId}`, true); | ||
const roles = permissions.map(p => p.role); | ||
const [permissions] = await serviceClient.get('acl', `/api/resourcePermissions/${serviceName}/${resourceGroupId}`, true); | ||
const roles = permissions.map(p => p.role); | ||
const splitTentant = { | ||
customerId : (tenantId && tenantId.startsWith('c_') && tenantId.substr(2)) || null, | ||
supplierId : (tenantId && tenantId.startsWith('s_') && tenantId.substr(2)) || null | ||
}; | ||
const splitTentant = { | ||
customerId: (tenantId && tenantId.startsWith('c_') && tenantId.substr(2)) || null, | ||
supplierId: (tenantId && tenantId.startsWith('s_') && tenantId.substr(2)) || null | ||
}; | ||
const tenants = new Set(); | ||
const tenants = new Set(); | ||
tenants.add(tenantId); | ||
tenants.add(tenantId); | ||
if(splitTentant.customerId) | ||
{ | ||
const [ customer ] = await serviceClient.get('business-partner', `/api/customers/${splitTentant.customerId}`, true); | ||
if (splitTentant.customerId) { | ||
const [customer] = await serviceClient.get('business-partner', `/api/customers/${splitTentant.customerId}`, true); | ||
/** | ||
/** | ||
* !Attention: Adding the hierarchy notation with "customerId/*" instead of the actual tenantId | ||
@@ -365,10 +348,9 @@ * to the list of tenants hacks the query in the user service to not return exact tenant | ||
*/ | ||
if(customer && customer.hierarchyId) | ||
customer.hierarchyId.split('|').forEach((c) => tenants.add(`c_${c}/*`)); | ||
} | ||
else if(splitTentant.supplierId) | ||
{ | ||
const [ supplier ] = await serviceClient.get('business-partner', `/api/suppliers/${splitTentant.supplierId}`, true); | ||
if (customer && customer.hierarchyId) { | ||
customer.hierarchyId.split('|').forEach((c) => tenants.add(`c_${c}/*`)); | ||
} | ||
} else if (splitTentant.supplierId) { | ||
const [supplier] = await serviceClient.get('business-partner', `/api/suppliers/${splitTentant.supplierId}`, true); | ||
/** | ||
/** | ||
* !Attention: Adding the hierarchy notation with "suplierId/*" instead of the actual tenantId | ||
@@ -379,16 +361,15 @@ * to the list of tenants hacks the query in the user service to not return exact tenant | ||
*/ | ||
if(supplier && supplier.hierarchyId) | ||
supplier.hierarchyId.split('|').forEach((s) => tenants.add(`s_${s}/*`)); | ||
} | ||
else | ||
{ | ||
throw new Error('The passed tenant identifier is not a valid tenant.'); | ||
} | ||
if (supplier && supplier.hierarchyId) { | ||
supplier.hierarchyId.split('|').forEach((s) => tenants.add(`s_${s}/*`)); | ||
} | ||
} else { | ||
throw new Error('The passed tenant identifier is not a valid tenant.'); | ||
} | ||
const [ users ] = await serviceClient.get('user', `/api/users?include=profile&roles=${roles.join(',')}&tenants=${Array.from(tenants).join(',')}`, true); | ||
const [users] = await serviceClient.get('user', `/api/users?include=profile&roles=${roles.join(',')}&tenants=${Array.from(tenants).join(',')}`, true); | ||
return users; | ||
} | ||
return users; | ||
} | ||
/** | ||
/** | ||
* Gets a list of all permissions granted to a set of roles. | ||
@@ -399,28 +380,24 @@ * @param {array} roles - List of roles to check. | ||
*/ | ||
async getPermissions(roles, serviceClient, serviceName) | ||
{ | ||
const all = await Promise.all(roles.map(async role => | ||
{ | ||
const url = `/api/permissions/${role}/${serviceName}`; | ||
const cacheKey = this.config.aclServiceName + ':' + url; | ||
const cached = await this.cache.get(cacheKey); | ||
async getPermissions(roles, serviceClient, serviceName) { | ||
const all = await Promise.all(roles.map(async role => { | ||
const url = `/api/permissions/${role}/${serviceName}`; | ||
const cacheKey = `${this.config.aclServiceName}:${url}`; | ||
const cached = await this.cache.get(cacheKey); | ||
return cached || serviceClient.get(this.config.aclServiceName, url).then(([ permissions ]) => | ||
{ | ||
this.cache.put(cacheKey, permissions, 600); | ||
return permissions; | ||
}) | ||
.catch(e => | ||
{ | ||
const message = (e.response && e.response.result && e.response.result.message) || e.message; | ||
const statusCode = (e.response && e.response.statusCode) || '-'; | ||
return cached || serviceClient.get(this.config.aclServiceName, url).then(([permissions]) => { | ||
this.cache.put(cacheKey, permissions, 600); | ||
return permissions; | ||
}). | ||
catch(e => { | ||
const message = (e.response && e.response.result && e.response.result.message) || e.message; | ||
const statusCode = (e.response && e.response.statusCode) || '-'; | ||
throw new Error(`Could not get service-role permissions: "${message}" (${statusCode})`); | ||
}); | ||
})); | ||
throw new Error(`Could not get service-role permissions: "${message}" (${statusCode})`); | ||
}); | ||
})); | ||
return all.reduce((all, more) => all.concat(more), [ ]); | ||
} | ||
return all.reduce((all, more) => all.concat(more), []); | ||
} | ||
/** | ||
/** | ||
* Gets a list of all tenants a user role is assigned to. | ||
@@ -439,46 +416,42 @@ * | ||
*/ | ||
async getUserTenants(userData, roleIds) | ||
{ | ||
let result; | ||
async getUserTenants(userData, roleIds) { | ||
let result; | ||
if(userData) | ||
{ | ||
const roleConstraints = roleIds && Array.isArray(userData.xroles) && userData.xroles.filter(r => roleIds.includes(r.role)).map(r => r.tenants); | ||
if (userData) { | ||
const roleConstraints = roleIds && | ||
Array.isArray(userData.xroles) && | ||
userData.xroles.filter(r => roleIds.includes(r.role)).map(r => r.tenants); | ||
if(userData.roles && userData.roles.indexOf('admin') > -1) | ||
result = [ '*' ]; | ||
else if(userData.id && userData.id.match(/^svc_[^@]*$/)) | ||
result = [ '*' ]; | ||
else if(Array.isArray(roleConstraints) && roleConstraints.length > 0) | ||
result = this.mergeRoleConstraints(roleConstraints); | ||
else if(userData.supplierid) | ||
result = [ `s_${userData.supplierid}` ]; | ||
else if(userData.customerid) | ||
result = [ `c_${userData.customerid}` ]; | ||
} | ||
return result || [ ]; | ||
if (userData.roles && userData.roles.indexOf('admin') > -1) { | ||
result = ['*']; | ||
} else if (userData.id && userData.id.match(/^svc_[^@]*$/)) { | ||
result = ['*']; | ||
} else if (Array.isArray(roleConstraints) && roleConstraints.length > 0) { | ||
result = this.mergeRoleConstraints(roleConstraints); | ||
} else if (userData.supplierid) { | ||
result = [`s_${userData.supplierid}`]; | ||
} else if (userData.customerid) { | ||
result = [`c_${userData.customerid}`]; | ||
} | ||
} | ||
mergeRoleConstraints(roleConstraints) | ||
{ | ||
let resultArray = [ ]; | ||
return result || []; | ||
} | ||
for(const roles of roleConstraints) | ||
{ | ||
if(roles.includes( '*' )) | ||
{ | ||
resultArray = [ '*' ]; | ||
break; | ||
} | ||
else | ||
{ | ||
resultArray = resultArray.concat(roles); | ||
} | ||
} | ||
mergeRoleConstraints(roleConstraints) { | ||
let resultArray = []; | ||
return [...new Set(resultArray)] | ||
for (const roles of roleConstraints) { | ||
if (roles.includes('*')) { | ||
resultArray = ['*']; | ||
break; | ||
} else { | ||
resultArray = resultArray.concat(roles); | ||
} | ||
} | ||
/** | ||
return [...new Set(resultArray)]; | ||
} | ||
/** | ||
* Gets a list of all tenants a user is assigned to for a given REST resource. | ||
@@ -500,16 +473,14 @@ * | ||
*/ | ||
async getUserTenantsByUrl(url, userData, serviceClient = null, method = 'GET', serviceName = null) | ||
{ | ||
const resources = await this.findResources(url, method, userData, serviceClient, serviceName); | ||
async getUserTenantsByUrl(url, userData, serviceClient = null, method = 'GET', serviceName = null) { | ||
const resources = await this.findResources(url, method, userData, serviceClient, serviceName); | ||
if(resources.length > 0) | ||
{ | ||
const resource = await this.mergeResources(resources); | ||
return (resource && this.getUserTenants(userData, resource.roleIds)) || [ ]; | ||
} | ||
return [ ]; | ||
if (resources.length > 0) { | ||
const resource = await this.mergeResources(resources); | ||
return (resource && this.getUserTenants(userData, resource.roleIds)) || []; | ||
} | ||
/** | ||
return []; | ||
} | ||
/** | ||
* Splits an array of tenant IDs into an array of objects containing a supplierId and a customerId. Whenever a | ||
@@ -522,97 +493,93 @@ * tenant is a supplier or a customer, the corresponding field is set. | ||
*/ | ||
splitUserTenants(tenants) | ||
{ | ||
if(!Array.isArray(tenants)) | ||
return [ ]; | ||
return tenants.map(tenantId => ({ | ||
supplierId : tenantId.startsWith('s_') ? tenantId.substr(2) : null, | ||
customerId : tenantId.startsWith('c_') ? tenantId.substr(2) : null | ||
})); | ||
splitUserTenants(tenants) { | ||
if (!Array.isArray(tenants)) { | ||
return []; | ||
} | ||
/** | ||
return tenants.map(tenantId => ({ | ||
supplierId: tenantId.startsWith('s_') ? tenantId.substr(2) : null, | ||
customerId: tenantId.startsWith('c_') ? tenantId.substr(2) : null | ||
})); | ||
} | ||
/** | ||
* Returns a boolean telling whenever a URL is considered a public resource. | ||
* @returns {boolean} Returns true or false. | ||
*/ | ||
isPublicResource(url) | ||
{ | ||
const { publicPaths } = this.config; | ||
isPublicResource(url) { | ||
const {publicPaths} = this.config; | ||
for(const i in publicPaths) | ||
{ | ||
if(url.match(new RegExp(publicPaths[i]))) | ||
return true; | ||
} | ||
return false; | ||
for (const i in publicPaths) { | ||
if (url.match(new RegExp(publicPaths[i]))) { | ||
return true; | ||
} | ||
} | ||
escapeRegExp(value) | ||
{ | ||
return value && value.replace(/[.*+?^${}()|[\]\\]/g, '$&'); | ||
} | ||
return false; | ||
} | ||
replacePlaceholders(resourceId, userData) | ||
{ | ||
const tenantId = (userData.supplierid && 's_' + userData.supplierid) | ||
|| (userData.customerid && 'c_' + userData.customerid); | ||
escapeRegExp(value) { | ||
return value && value.replace(/[.*+?^${}()|[\]\\]/g, '$&'); | ||
} | ||
return resourceId.replace(/\${_current_tenant_id}/g, this.escapeRegExp(tenantId)) // TODO: Remove this after full migration to Business Partner | ||
.replace(/\${_current_user_id}/g, this.escapeRegExp(userData.id)) | ||
.replace(/\${_current_customer_id}/g, this.escapeRegExp(userData.customerid)) // TODO: Remove this after full migration to Business Partner | ||
.replace(/\${_current_supplier_id}/g, this.escapeRegExp(userData.supplierid)) // TODO: Remove this after full migration to Business Partner | ||
.replace(/\${_current_business_partner_id}/g, this.escapeRegExp(userData.businesspartner && userData.businesspartner.id)); | ||
} | ||
replacePlaceholders(resourceId, userData) { | ||
const tenantId = (userData.supplierid && `s_${userData.supplierid}`) || | ||
(userData.customerid && `c_${userData.customerid}`); | ||
buildRecusiveResult(keys, values) | ||
{ | ||
if(keys && keys.length > 0) | ||
{ | ||
keys = [ ].concat(keys); | ||
const key = keys.shift(); | ||
return resourceId.replace(/\${_current_tenant_id}/g, this.escapeRegExp(tenantId)). // TODO: Remove this after full migration to Business Partner | ||
replace(/\${_current_user_id}/g, this.escapeRegExp(userData.id)). | ||
replace(/\${_current_customer_id}/g, this.escapeRegExp(userData.customerid)). // TODO: Remove this after full migration to Business Partner | ||
replace(/\${_current_supplier_id}/g, this.escapeRegExp(userData.supplierid)). // TODO: Remove this after full migration to Business Partner | ||
replace(/\${_current_business_partner_id}/g, | ||
this.escapeRegExp(userData.businesspartner && userData.businesspartner.id) | ||
); | ||
} | ||
if(typeof values[key] !== 'undefined') | ||
return { [key]: this.buildRecusiveResult(keys, values[key]) }; | ||
buildRecusiveResult(keys, values) { | ||
if (keys && keys.length > 0) { | ||
keys = [].concat(keys); | ||
const key = keys.shift(); | ||
return { }; | ||
} | ||
if (typeof values[key] !== 'undefined') { | ||
return {[key]: this.buildRecusiveResult(keys, values[key])}; | ||
} | ||
return keys && values; | ||
return { }; | ||
} | ||
deleteRecursiveResult(keys, values) | ||
{ | ||
if(keys) | ||
{ | ||
keys = [ ].concat(keys); | ||
const key = keys.shift(); | ||
return keys && values; | ||
} | ||
if(keys.length > 0) | ||
this.deleteRecursiveResult(keys, values[key]); | ||
else if(key && typeof(values) === 'object') | ||
delete values[key]; | ||
} | ||
deleteRecursiveResult(keys, values) { | ||
if (keys) { | ||
keys = [].concat(keys); | ||
const key = keys.shift(); | ||
return values; | ||
if (keys.length > 0) { | ||
this.deleteRecursiveResult(keys, values[key]); | ||
} else if (key && typeof(values) === 'object') { | ||
delete values[key]; | ||
} | ||
} | ||
applyStructureFilter(keyList, values) | ||
{ | ||
return keyList.reduce((all, key) => extend(true, all, this.buildRecusiveResult(key && key.split('.'), values)), { }); | ||
} | ||
return values; | ||
} | ||
applyBlackFilter(keyList, values) | ||
{ | ||
keyList.forEach(key => this.deleteRecursiveResult(key && key.split('.'), values)); | ||
return values; | ||
} | ||
applyStructureFilter(keyList, values) { | ||
return keyList. | ||
reduce((all, key) => extend(true, all, this.buildRecusiveResult(key && key.split('.'), values)), { }); | ||
} | ||
checkAlwaysAllow(roles) | ||
{ | ||
const { alwaysAllow, alwaysDeny } = this.config.roles; | ||
return roles.findIndex(r => alwaysAllow.findIndex(a => r.match(a)) >= 0 && alwaysDeny.findIndex(a => r.match(a)) === -1) >= 0; | ||
} | ||
applyBlackFilter(keyList, values) { | ||
keyList.forEach(key => this.deleteRecursiveResult(key && key.split('.'), values)); | ||
return values; | ||
} | ||
/** | ||
checkAlwaysAllow(roles) { | ||
const {alwaysAllow, alwaysDeny} = this.config.roles; | ||
return roles. | ||
findIndex(r => alwaysAllow.findIndex(a => r.match(a)) >= 0 && alwaysDeny.findIndex(a => r.match(a)) === -1) >= 0; | ||
} | ||
/** | ||
* Filters an object or array of objects recursively and returns a copy. | ||
@@ -627,47 +594,50 @@ * Passing a whiteKeys array carries only keys from there to the result. | ||
*/ | ||
filterObject(obj, whiteKeys, blackKeys) | ||
{ | ||
if(Array.isArray(obj)) | ||
return obj.map(o => this.filterObject(o, whiteKeys, blackKeys)).filter(o => o && Object.keys(o).length > 0); | ||
filterObject(obj, whiteKeys, blackKeys) { | ||
if (Array.isArray(obj)) { | ||
return obj.map(o => this.filterObject(o, whiteKeys, blackKeys)).filter(o => o && Object.keys(o).length > 0); | ||
} | ||
let result = obj && typeof obj === 'object' && extend(true, { }, obj.dataValues || obj); | ||
if(Array.isArray(whiteKeys) && result) | ||
result = this.applyStructureFilter(whiteKeys, result); | ||
if(Array.isArray(blackKeys) && result) | ||
result = this.applyBlackFilter(blackKeys, result); | ||
let result = obj && typeof obj === 'object' && extend(true, { }, obj.dataValues || obj); | ||
return result || obj; | ||
if (Array.isArray(whiteKeys) && result) { | ||
result = this.applyStructureFilter(whiteKeys, result); | ||
} | ||
wrapCallback(callback, whiteKeys, blackKeys) | ||
{ | ||
return (obj) => callback(this.filterObject(obj, whiteKeys, blackKeys)); | ||
if (Array.isArray(blackKeys) && result) { | ||
result = this.applyBlackFilter(blackKeys, result); | ||
} | ||
mergeResources(resources) | ||
{ | ||
const result = extend(true, { }, ...resources); | ||
result.roleIds = [ ]; | ||
return result || obj; | ||
} | ||
for(const res of resources) | ||
{ | ||
if(!res.requestFields || !res.requestFields.allow) | ||
delete result.requestFields.allow; | ||
if(!res.requestFields || !res.requestFields.remove) | ||
delete result.requestFields.remove; | ||
if(!res.responseFields || !res.responseFields.allow) | ||
delete result.responseFields.allow; | ||
if(!res.responseFields || !res.responseFields.remove) | ||
delete result.responseFields.remove; | ||
wrapCallback(callback, whiteKeys, blackKeys) { | ||
return (obj) => callback(this.filterObject(obj, whiteKeys, blackKeys)); | ||
} | ||
result.roleIds = result.roleIds.concat(res.roleIds); | ||
} | ||
mergeResources(resources) { | ||
const result = extend(true, { }, ...resources); | ||
result.roleIds = []; | ||
result.roleIds = [ ...new Set(result.roleIds) ]; | ||
for (const res of resources) { | ||
if (!res.requestFields || !res.requestFields.allow) { | ||
delete result.requestFields.allow; | ||
} | ||
if (!res.requestFields || !res.requestFields.remove) { | ||
delete result.requestFields.remove; | ||
} | ||
if (!res.responseFields || !res.responseFields.allow) { | ||
delete result.responseFields.allow; | ||
} | ||
if (!res.responseFields || !res.responseFields.remove) { | ||
delete result.responseFields.remove; | ||
} | ||
return result; | ||
result.roleIds = result.roleIds.concat(res.roleIds); | ||
} | ||
/** | ||
result.roleIds = [...new Set(result.roleIds)]; | ||
return result; | ||
} | ||
/** | ||
* Returns a middleware to be used with express. | ||
@@ -678,95 +648,85 @@ * The middleware returned by this method will automatically provide endpoint security, input | ||
*/ | ||
middleware() | ||
{ | ||
return (req, res, next) => | ||
{ | ||
const url = req.originalUrl.split('?')[0]; | ||
middleware() { | ||
return (req, res, next) => { | ||
const url = req.originalUrl.split('?')[0]; | ||
if(this.isPublicResource(url)) | ||
{ | ||
/** TODO: Remove this after full migration to Business Partner --> */ | ||
// Normally you should NOT use util.deprecate - see discussions on the node forum for details | ||
/** | ||
if (this.isPublicResource(url)) { | ||
/** TODO: Remove this after full migration to Business Partner --> */ | ||
// Normally you should NOT use util.deprecate - see discussions on the node forum for details | ||
/** | ||
* @deprecated Use getUserBusinessPartnerIds instead | ||
*/ | ||
req.opuscapita.getUserTenants = async () => util.deprecate(() => [ ], 'getUserTenants() will be removed in upcomming version 2.x - returning empty array')(); | ||
req.opuscapita.getUserTenants = async () => util.deprecate(() => [], 'getUserTenants() will be removed in upcomming version 2.x - returning empty array')(); | ||
/** | ||
/** | ||
* @deprecated Use getUserBusinessPartnerIdsByUrl instead | ||
*/ | ||
req.opuscapita.getUserTenantsByUrl = async () => util.deprecate(() => [ ], 'getUserTenantsByUrl() will be removed in upcomming version 2.x - returning empty array')(); | ||
req.opuscapita.getUserTenantsByUrl = async () => util.deprecate(() => [], 'getUserTenantsByUrl() will be removed in upcomming version 2.x - returning empty array')(); | ||
/** | ||
/** | ||
* @deprecated New business partners are allowed to be customer and supplier at the same time | ||
*/ | ||
req.opuscapita.splitUserTenants = (tenants) => this.splitUserTenants(tenants); | ||
req.opuscapita.splitUserTenants = (tenants) => this.splitUserTenants(tenants); | ||
/** | ||
/** | ||
* @deprecated Use getUsersByPermissionAndBusinessPartner instead | ||
*/ | ||
req.opuscapita.getUsersByPermissionAndTenant = async (serviceName, resourceGroupId, tenantId) => this.getUsersByPermissionAndTenant(serviceName, resourceGroupId, tenantId, req.opuscapita.serviceClient); | ||
/** <-- TODO: Remove this after full migration to Business Partner */ | ||
req.opuscapita.getUserBusinessPartnerIds = async () => [ ]; | ||
req.opuscapita.getUserBusinessPartnerIdsByUrl = async () => [ ]; | ||
req.opuscapita.getUsersByPermissionAndBusinessPartner = async (serviceName, resourceGroupId, businessPartnerId) => this.getUsersByPermissionAndTenant(serviceName, resourceGroupId, businessPartnerId, req.opuscapita.serviceClient); | ||
req.opuscapita.getUsersByPermissionAndTenant = async (serviceName, resourceGroupId, tenantId) => this.getUsersByPermissionAndTenant(serviceName, resourceGroupId, tenantId, req.opuscapita.serviceClient); | ||
/** <-- TODO: Remove this after full migration to Business Partner */ | ||
next(); | ||
} | ||
else | ||
{ | ||
const method = req.method; | ||
const userData = req.opuscapita.userData(); | ||
const serviceClient = req.opuscapita.serviceClient; | ||
req.opuscapita.getUserBusinessPartnerIds = async () => []; | ||
req.opuscapita.getUserBusinessPartnerIdsByUrl = async () => []; | ||
req.opuscapita.getUsersByPermissionAndBusinessPartner = async (serviceName, resourceGroupId, businessPartnerId) => this.getUsersByPermissionAndTenant(serviceName, resourceGroupId, businessPartnerId, req.opuscapita.serviceClient); | ||
this.findResources(url, method, userData, serviceClient).then(resources => | ||
{ | ||
if(resources.length) | ||
{ | ||
const resource = this.mergeResources(resources); | ||
next(); | ||
} else { | ||
const method = req.method; | ||
const userData = req.opuscapita.userData(); | ||
const serviceClient = req.opuscapita.serviceClient; | ||
{ | ||
const { allow, remove } = resource.requestFields || { }; | ||
this.findResources(url, method, userData, serviceClient).then(resources => { | ||
if (resources.length) { | ||
const resource = this.mergeResources(resources); | ||
/** TODO: Remove this after full migration to Business Partner --> */ | ||
req.opuscapita.getUserTenants = async () => this.getUserTenants(req.opuscapita.userData(), resource.roleIds); | ||
req.opuscapita.getUserTenantsByUrl = async (url, serviceName = null) => this.getUserTenantsByUrl(url, req.opuscapita.userData(), serviceClient, method, serviceName); | ||
req.opuscapita.splitUserTenants = (tenants) => this.splitUserTenants(tenants); | ||
req.opuscapita.getUsersByPermissionAndTenant = async (serviceName, resourceGroupId, tenantId) => this.getUsersByPermissionAndTenant(serviceName, resourceGroupId, tenantId, req.opuscapita.serviceClient); | ||
/** <-- TODO: Remove this after full migration to Business Partner */ | ||
{ | ||
const {allow, remove} = resource.requestFields || { }; | ||
req.opuscapita.getUserBusinessPartnerIds = async () => this.getUserBusinessPartnerIds(req.opuscapita.userData(), resource.roleIds); | ||
req.opuscapita.getUserBusinessPartnerIdsByUrl = async (url, serviceName = null) => this.getUserBusinessPartnerIdsByUrl(url, req.opuscapita.userData(), serviceClient, method, serviceName); | ||
req.opuscapita.getUsersByPermissionAndBusinessPartner = async (serviceName, resourceGroupId, businessPartnerId) => this.getUsersByPermissionAndTenant(serviceName, resourceGroupId, businessPartnerId, req.opuscapita.serviceClient); | ||
/** TODO: Remove this after full migration to Business Partner --> */ | ||
req.opuscapita.getUserTenants = async () => this.getUserTenants(req.opuscapita.userData(), resource.roleIds); | ||
req.opuscapita.getUserTenantsByUrl = async (url, serviceName = null) => this.getUserTenantsByUrl(url, req.opuscapita.userData(), serviceClient, method, serviceName); | ||
req.opuscapita.splitUserTenants = (tenants) => this.splitUserTenants(tenants); | ||
req.opuscapita.getUsersByPermissionAndTenant = async (serviceName, resourceGroupId, tenantId) => this.getUsersByPermissionAndTenant(serviceName, resourceGroupId, tenantId, req.opuscapita.serviceClient); | ||
/** <-- TODO: Remove this after full migration to Business Partner */ | ||
req.body = req.body && this.filterObject(req.body, allow, remove); | ||
} | ||
req.opuscapita.getUserBusinessPartnerIds = async () => this.getUserBusinessPartnerIds(req.opuscapita.userData(), resource.roleIds); | ||
req.opuscapita.getUserBusinessPartnerIdsByUrl = async (url, serviceName = null) => this.getUserBusinessPartnerIdsByUrl(url, req.opuscapita.userData(), serviceClient, method, serviceName); | ||
req.opuscapita.getUsersByPermissionAndBusinessPartner = async (serviceName, resourceGroupId, businessPartnerId) => this.getUsersByPermissionAndTenant(serviceName, resourceGroupId, businessPartnerId, req.opuscapita.serviceClient); | ||
{ | ||
const { allow, remove } = resource.responseFields || { }; | ||
req.body = req.body && this.filterObject(req.body, allow, remove); | ||
} | ||
res.json = this.wrapCallback(res.json.bind(res), allow, remove); | ||
res.jsonp = this.wrapCallback(res.jsonp.bind(res), allow, remove); | ||
} | ||
{ | ||
const {allow, remove} = resource.responseFields || { }; | ||
next(); | ||
} | ||
else | ||
{ | ||
const message = `You do not have permissions to access the requested resource: ${url}`; | ||
res.json = this.wrapCallback(res.json.bind(res), allow, remove); | ||
res.jsonp = this.wrapCallback(res.jsonp.bind(res), allow, remove); | ||
} | ||
req.opuscapita.logger.warn('No permissions to access resource', {url, method, userData}); | ||
res.status(403).json({ message }); | ||
} | ||
}) | ||
.catch(error => | ||
{ | ||
const message = 'Access was denied due to an internal authentication error.'; | ||
next(); | ||
} else { | ||
const message = `You do not have permissions to access the requested resource: ${url}`; | ||
req.opuscapita.logger.error(message, {error, url, method, userData }); | ||
res.status(403).json({ message }); | ||
}); | ||
} | ||
} | ||
} | ||
req.opuscapita.logger.warn('No permissions to access resource', {url, method, userData}); | ||
res.status(403).json({message}); | ||
} | ||
}). | ||
catch(error => { | ||
const message = 'Access was denied due to an internal authentication error.'; | ||
req.opuscapita.logger.error(message, {error, url, method, userData}); | ||
res.status(403).json({message}); | ||
}); | ||
} | ||
}; | ||
} | ||
} | ||
@@ -789,20 +749,20 @@ | ||
Bouncer.DefaultConfig = { | ||
serviceName : config.serviceName, | ||
permissions : process.cwd() + '/src/server/acl.json', | ||
aclServiceName : 'acl', | ||
logger : new Logger({context:{name:"bouncer"}}), | ||
roles : { | ||
alwaysAllow : [ ], | ||
alwaysDeny : [ ] | ||
}, | ||
publicPaths : [ | ||
'^/public', | ||
'^/static', | ||
'^/api/health/stats$', | ||
'^/api/health/check$', | ||
'^/api/health/metrics$', | ||
'^/api/list/apis$' | ||
] | ||
} | ||
serviceName: config.serviceName, | ||
permissions: `${process.cwd()}/src/server/acl.json`, | ||
aclServiceName: 'acl', | ||
logger: new Logger({context: {name: 'bouncer'}}), | ||
roles: { | ||
alwaysAllow: [], | ||
alwaysDeny: [] | ||
}, | ||
publicPaths: [ | ||
'^/public', | ||
'^/static', | ||
'^/api/health/stats$', | ||
'^/api/health/check$', | ||
'^/api/health/metrics$', | ||
'^/api/list/apis$' | ||
] | ||
}; | ||
module.exports = Bouncer; |
{ | ||
"name": "@opuscapita/bouncer", | ||
"version": "1.2.5", | ||
"version": "1.2.6", | ||
"description": "API and express middleware for OpusCapita ACl service based access security.", | ||
@@ -36,2 +36,3 @@ "main": "index.js", | ||
"@opuscapita/db-init": "^3.0.18", | ||
"@opuscapita/eslint-config-opuscapita-bnapp": "^1.3.5", | ||
"@opuscapita/web-init": "^4.2.0", | ||
@@ -38,0 +39,0 @@ "@types/node": "^18.0.6", |
44969
8
633