From 5cad0297ede1a9b757d54c10701144e71eb0dd03 Mon Sep 17 00:00:00 2001 From: akumar Date: Tue, 21 Nov 2023 16:09:57 +0100 Subject: [PATCH] fix: Add support for multiple operations --- src/core/hierarchicalScope.ts | 32 +++++------ test/core.spec.ts | 8 +-- test/fixtures/multiple_operations.yml | 47 ++++++++++++++++ test/properties.spec.ts | 78 +++++++++++++++++++++++---- test/utils.ts | 26 ++++++--- 5 files changed, 151 insertions(+), 40 deletions(-) create mode 100644 test/fixtures/multiple_operations.yml diff --git a/src/core/hierarchicalScope.ts b/src/core/hierarchicalScope.ts index 4eb1116..01e4fe9 100644 --- a/src/core/hierarchicalScope.ts +++ b/src/core/hierarchicalScope.ts @@ -49,21 +49,21 @@ export const checkHierarchicalScope = async (ruleTarget: Target, const ctxResources = context.resources || []; const reqTarget = request.target; - let currentResourceEntity: string; + let entityOrOperation: string; // iterating through all targeted resources and retrieve relevant owners instances for (let attribute of ruleTarget.resources || []) { if (attribute?.id == urns.get('entity')) { // resource type found logger.debug('Evaluating resource entity match'); - currentResourceEntity = attribute?.value; + entityOrOperation = attribute?.value; let entitiesMatch = false; // iterating request resources to filter all resources of a given type for (let requestAttribute of reqTarget.resources || []) { - if (requestAttribute?.id == attribute?.id && requestAttribute?.value == currentResourceEntity) { + if (requestAttribute?.id == attribute?.id && requestAttribute?.value == entityOrOperation) { entitiesMatch = true; // a resource entity that matches the request and the rule's target } else if (requestAttribute?.id == attribute?.id) { // rule entity, get ruleNS and entityRegexValue for rule - const value = currentResourceEntity; + const value = entityOrOperation; let pattern = value?.substring(value?.lastIndexOf(':') + 1); let nsEntityArray = pattern?.split('.'); // firstElement could be either entity or namespace @@ -104,7 +104,6 @@ export const checkHierarchicalScope = async (ruleTarget: Target, const instanceID = requestAttribute?.value; // found resource instance ID, iterating through the context to check if owners entities match the scoping entities let ctxResource: Resource = _.find(ctxResources, ['instance.id', instanceID]); - // ctxResource = ctxResource.instance; if (ctxResource) { ctxResource = ctxResource?.instance; } else { @@ -122,38 +121,33 @@ export const checkHierarchicalScope = async (ruleTarget: Target, logger.debug('Resource of targeted entity was not provided in context'); return false; // resource of targeted entity was not provided in context } - // entitiesMatch = false; } } } else if (attribute?.id === urns.get('operation')) { logger.debug('Evaluating resource operation match'); - currentResourceEntity = attribute?.value; + entityOrOperation = attribute?.value; for (let reqAttribute of reqTarget.resources || []) { // match Rule resource operation URN and operation name with request resource operation URN and operation name if (reqAttribute?.id === attribute?.id && reqAttribute?.value === attribute?.value) { - if (ctxResources?.length === 1) { - let meta; - if (ctxResources[0]?.instance) { - meta = ctxResources[0]?.instance?.meta; - } else if (ctxResources[0]?.meta) { - meta = ctxResources[0].meta; - } - + // find context resource based + let ctxResource: Resource = _.find(ctxResources, ['id', entityOrOperation]); + if (ctxResource) { + const meta = ctxResource.meta; if (_.isEmpty(meta) || _.isEmpty(meta.owners)) { - logger.debug(`Owner information missing for hierarchical scope matching of operation ${attribute.value}, evaluation fails`); + logger.debug(`Owners information missing for hierarchical scope matching of entity ${attribute.value}, evaluation fails`); return false; // no ownership was passed, evaluation fails } scopedRoles = updateScopedRoles(meta, scopedRoles, urns, totalScopingEntities); } else { - logger.debug('Invalid resource passed', { resource: ctxResources }); - return false; + logger.debug('Operation name was not provided in context'); + return false; // Operation name was not provided in context } } } } } - if (_.isNil(currentResourceEntity) || _.isEmpty(currentResourceEntity)) { + if (_.isNil(entityOrOperation) || _.isEmpty(entityOrOperation)) { logger.debug('No Entity or operation name found'); return false; // no entity found } diff --git a/test/core.spec.ts b/test/core.spec.ts index 4a11fc3..7971632 100644 --- a/test/core.spec.ts +++ b/test/core.spec.ts @@ -7,7 +7,7 @@ import { createServiceConfig } from '@restorecommerce/service-config'; import { createLogger } from '@restorecommerce/logger'; import { Events } from '@restorecommerce/kafka-client'; import { createChannel, createClient } from '@restorecommerce/grpc-client'; -import { UserServiceDefinition, UserServiceClient } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/user'; +import { UserServiceDefinition, UserServiceClient } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/user'; import { Request, Response, Response_Decision } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/access_control'; const cfg = createServiceConfig(process.cwd() + '/test'); @@ -517,7 +517,7 @@ describe('Testing access control core', () => { roleScopingEntity: 'urn:restorecommerce:acs:model:organization.Organization', roleScopingInstance: 'Org1', resourceType: 'mutation.executeTestMutation', - resourceID: 'TestMutate 1', + resourceID: 'mutation.executeTestMutation', actionType: 'urn:restorecommerce:acs:names:action:execute', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', ownerInstance: 'Org1' @@ -531,7 +531,7 @@ describe('Testing access control core', () => { roleScopingEntity: 'urn:restorecommerce:acs:model:organization.Organization', roleScopingInstance: 'Org2', resourceType: 'mutation.executeTestMutation', - resourceID: 'TestMutate 1', + resourceID: 'mutation.executeTestMutation', actionType: 'urn:restorecommerce:acs:names:action:execute', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', ownerInstance: 'Org1' @@ -545,7 +545,7 @@ describe('Testing access control core', () => { roleScopingEntity: 'urn:restorecommerce:acs:model:organization.Organization', roleScopingInstance: 'Org1', resourceType: 'mutation.executeTestMutation', - resourceID: 'TestMutate 1', + resourceID: 'mutation.executeTestMutation', actionType: 'urn:restorecommerce:acs:names:action:execute', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', ownerInstance: 'Org1' diff --git a/test/fixtures/multiple_operations.yml b/test/fixtures/multiple_operations.yml new file mode 100644 index 0000000..c21d280 --- /dev/null +++ b/test/fixtures/multiple_operations.yml @@ -0,0 +1,47 @@ + policy_sets: + - id: policySet + name: Global policy set containing 2 policies + description: A policy set + combining_algorithm: urn:oasis:names:tc:xacml:3.0:rule-combining-algorithm:deny-overrides + policies: + - id: ExecutePolicy + name: Execute Policy + description: Policy with two rules targeting two Operations + combining_algorithm: urn:oasis:names:tc:xacml:3.0:rule-combining-algorithm:permit-overrides + rules: + - id: ruleAA1 + name: Rule AA1 + descripton: A rule targeting a execute action by a SimpleUser for `mutation.Test1` + target: + subjects: + - id: urn:restorecommerce:acs:names:role + value: SimpleUser + - id: urn:restorecommerce:acs:names:roleScopingEntity + value: urn:restorecommerce:acs:model:organization.Organization + resources: + - id: urn:restorecommerce:acs:names:operation + value: mutation.Test1 + actions: + - id: urn:oasis:names:tc:xacml:1.0:action:action-id + value: urn:restorecommerce:acs:names:action:execute + effect: PERMIT + - id: ruleAA2 + name: Rule AA2 + descripton: A rule targeting a execute action by a SimpleUser for `mutation.Test2` + target: + subjects: + - id: urn:restorecommerce:acs:names:role + value: SimpleUser + - id: urn:restorecommerce:acs:names:roleScopingEntity + value: urn:restorecommerce:acs:model:organization.Organization + resources: + - id: urn:restorecommerce:acs:names:operation + value: mutation.Test1 + actions: + - id: urn:oasis:names:tc:xacml:1.0:action:action-id + value: urn:restorecommerce:acs:names:action:execute + effect: PERMIT + - id: ruleAA3 + name: Fallback rule + description: Always deny + effect: DENY \ No newline at end of file diff --git a/test/properties.spec.ts b/test/properties.spec.ts index feb8b33..392c635 100644 --- a/test/properties.spec.ts +++ b/test/properties.spec.ts @@ -141,6 +141,64 @@ describe('testing access control', () => { after(async () => { await worker.stop(); }); + + describe('testing isAllowed with multiple entities and different properties in each entity', () => { + before(async () => { + // disable authorization + cfg.set('authorization:enabled', false); + cfg.set('authorization:enforce', false); + updateConfig(cfg); + await create('./test/fixtures/multiple_operations.yml'); + }); + after(async () => { + await truncate(); + }); + + it('should DENY executing multiple Operations for target scope which subject does not have access', async () => { + const accessRequest = testUtils.buildRequest({ + subjectID: 'Alice', + subjectRole: 'SimpleUser', + roleScopingEntity: 'urn:restorecommerce:acs:model:organization.Organization', + roleScopingInstance: 'Org2', + resourceType: ['mutation.Test1', 'mutation.Test2'], + resourceID: ['mutation.Test1', 'mutation.Test2'], + actionType: 'urn:restorecommerce:acs:names:action:execute', + ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', + ownerInstance: ['Org1', 'Org1'] + }); + testUtils.marshallRequest(accessRequest); + + const result = await accessControlService.isAllowed(accessRequest); + should.exist(result); + should.exist(result.decision); + result.decision.should.equal(Response_Decision.DENY); + result.operation_status.code.should.equal(200); + result.operation_status.message.should.equal('success'); + }); + + it('should PERMIT executing multiple Operations for target scope which subject has access', async () => { + const accessRequest = testUtils.buildRequest({ + subjectID: 'Alice', + subjectRole: 'SimpleUser', + roleScopingEntity: 'urn:restorecommerce:acs:model:organization.Organization', + roleScopingInstance: 'Org1', + resourceType: ['mutation.Test1', 'mutation.Test2'], + resourceID: ['mutation.Test1', 'mutation.Test2'], + actionType: 'urn:restorecommerce:acs:names:action:execute', + ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', + ownerInstance: ['Org1', 'Org1'] + }); + testUtils.marshallRequest(accessRequest); + + const result = await accessControlService.isAllowed(accessRequest); + should.exist(result); + should.exist(result.decision); + result.decision.should.equal(Response_Decision.PERMIT); + result.operation_status.code.should.equal(200); + result.operation_status.message.should.equal('success'); + }); + }); + describe('isAllowed() for single entity', () => { before(async () => { await create('./test/fixtures/properties.yml'); @@ -868,7 +926,7 @@ describe('testing access control', () => { roleScopingInstance: 'Org1', resourceType: ['urn:restorecommerce:acs:model:location.Location', 'urn:restorecommerce:acs:model:organization.Organization'], resourceProperty: [['urn:restorecommerce:acs:model:location.Location#locid', 'urn:restorecommerce:acs:model:location.Location#locname'], - ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname']], + ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname']], resourceID: ['Bob', 'Org'], actionType: 'urn:restorecommerce:acs:names:action:read', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', @@ -913,7 +971,7 @@ describe('testing access control', () => { roleScopingInstance: 'Org1', resourceType: ['urn:restorecommerce:acs:model:location.Location', 'urn:restorecommerce:acs:model:organization.Organization'], resourceProperty: [['urn:restorecommerce:acs:model:location.Location#locid', 'urn:restorecommerce:acs:model:location.Location#locname'], - ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname', 'urn:restorecommerce:acs:model:organization.Organization#orgdescription']], + ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname', 'urn:restorecommerce:acs:model:organization.Organization#orgdescription']], resourceID: ['Bob', 'Org'], actionType: 'urn:restorecommerce:acs:names:action:read', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', @@ -958,7 +1016,7 @@ describe('testing access control', () => { roleScopingInstance: 'Org1', resourceType: ['urn:restorecommerce:acs:model:location.Location', 'urn:restorecommerce:acs:model:organization.Organization'], resourceProperty: [['urn:restorecommerce:acs:model:location.Location#locid', 'urn:restorecommerce:acs:model:location.Location#locname'], - ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname']], + ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname']], resourceID: ['Bob', 'Org'], actionType: 'urn:restorecommerce:acs:names:action:modify', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', @@ -1003,7 +1061,7 @@ describe('testing access control', () => { roleScopingInstance: 'Org1', resourceType: ['urn:restorecommerce:acs:model:location.Location', 'urn:restorecommerce:acs:model:organization.Organization'], resourceProperty: [['urn:restorecommerce:acs:model:location.Location#locid', 'urn:restorecommerce:acs:model:location.Location#locname'], - ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname', 'urn:restorecommerce:acs:model:organization.Organization#orgdescription']], + ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname', 'urn:restorecommerce:acs:model:organization.Organization#orgdescription']], resourceID: ['Bob', 'Org'], actionType: 'urn:restorecommerce:acs:names:action:modify', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', @@ -1060,7 +1118,7 @@ describe('testing access control', () => { roleScopingInstance: 'Org1', resourceType: ['urn:restorecommerce:acs:model:location.Location', 'urn:restorecommerce:acs:model:organization.Organization'], resourceProperty: [['urn:restorecommerce:acs:model:location.Location#locid', 'urn:restorecommerce:acs:model:location.Location#locname'], - ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname']], + ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname']], resourceID: ['Bob', 'Org'], actionType: 'urn:restorecommerce:acs:names:action:read', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', @@ -1123,7 +1181,7 @@ describe('testing access control', () => { roleScopingInstance: 'Org1', resourceType: ['urn:restorecommerce:acs:model:location.Location', 'urn:restorecommerce:acs:model:organization.Organization'], resourceProperty: [['urn:restorecommerce:acs:model:location.Location#locid', 'urn:restorecommerce:acs:model:location.Location#locname', 'urn:restorecommerce:acs:model:location.Location#locdescription'], - ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname', 'urn:restorecommerce:acs:model:organization.Organization#orgdescription']], + ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname', 'urn:restorecommerce:acs:model:organization.Organization#orgdescription']], resourceID: ['Bob', 'Org'], actionType: 'urn:restorecommerce:acs:names:action:read', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', @@ -1207,7 +1265,7 @@ describe('testing access control', () => { roleScopingInstance: 'Org1', resourceType: ['urn:restorecommerce:acs:model:location.Location', 'urn:restorecommerce:acs:model:organization.Organization'], resourceProperty: [['urn:restorecommerce:acs:model:location.Location#locid', 'urn:restorecommerce:acs:model:location.Location#locname'], - ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname']], + ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname']], resourceID: ['Bob', 'Org'], actionType: 'urn:restorecommerce:acs:names:action:read', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', @@ -1230,7 +1288,7 @@ describe('testing access control', () => { roleScopingInstance: 'Org1', resourceType: ['urn:restorecommerce:acs:model:location.Location', 'urn:restorecommerce:acs:model:organization.Organization'], resourceProperty: [['urn:restorecommerce:acs:model:location.Location#locid', 'urn:restorecommerce:acs:model:location.Location#locname'], - ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname', 'urn:restorecommerce:acs:model:organization.Organization#orgdescription']], + ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname', 'urn:restorecommerce:acs:model:organization.Organization#orgdescription']], resourceID: ['Bob', 'Org'], actionType: 'urn:restorecommerce:acs:names:action:read', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', @@ -1287,7 +1345,7 @@ describe('testing access control', () => { roleScopingInstance: 'Org1', resourceType: ['urn:restorecommerce:acs:model:location.Location', 'urn:restorecommerce:acs:model:organization.Organization'], resourceProperty: [['urn:restorecommerce:acs:model:location.Location#locid', 'urn:restorecommerce:acs:model:location.Location#locname'], - ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname']], + ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname']], resourceID: ['Bob', 'Org'], actionType: 'urn:restorecommerce:acs:names:action:read', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', @@ -1315,7 +1373,7 @@ describe('testing access control', () => { roleScopingInstance: 'Org1', resourceType: ['urn:restorecommerce:acs:model:location.Location', 'urn:restorecommerce:acs:model:organization.Organization'], resourceProperty: [['urn:restorecommerce:acs:model:location.Location#locid', 'urn:restorecommerce:acs:model:location.Location#locname'], - ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname', 'urn:restorecommerce:acs:model:organization.Organization#orgdescription']], + ['urn:restorecommerce:acs:model:organization.Organization#orgid', 'urn:restorecommerce:acs:model:organization.Organization#orgname', 'urn:restorecommerce:acs:model:organization.Organization#orgdescription']], resourceID: ['Bob', 'Org'], actionType: 'urn:restorecommerce:acs:names:action:read', ownerIndicatoryEntity: 'urn:restorecommerce:acs:model:organization.Organization', diff --git a/test/utils.ts b/test/utils.ts index ea0d788..14a4cfa 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -47,13 +47,25 @@ export const buildRequest = (opts: RequestOpts): Request => { } if (opts.actionType === 'urn:restorecommerce:acs:names:action:execute') { - resources = resources.concat([ - { - id: 'urn:restorecommerce:acs:names:operation', - value: opts.resourceType as string, - attributes: [] - } - ]); + if (typeof opts.resourceType === 'string') { + resources = resources.concat([ + { + id: 'urn:restorecommerce:acs:names:operation', + value: opts.resourceType as string, + attributes: [] + } + ]); + } else { + opts.resourceType.forEach((operationName) => { + resources = resources.concat([ + { + id: 'urn:restorecommerce:acs:names:operation', + value: operationName, + attributes: [] + } + ]); + }); + } } else { if (typeof opts.resourceType === 'string') { resources = resources.concat([