Skip to content

Commit

Permalink
fix(acs-client): added unit test for validating policy filters for ma…
Browse files Browse the repository at this point in the history
…tching both Org and User scoping entities case
  • Loading branch information
Arun-KumarH committed Apr 19, 2024
1 parent 4debc8b commit 89319a4
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 57 deletions.
4 changes: 4 additions & 0 deletions packages/acs-client/cfg/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@
{
"scopingEntity": "urn:test:acs:model:organization.Organization",
"value": "orgKey"
},
{
"scopingEntity": "urn:test:acs:model:user.User",
"value": "userKey"
}
],
"hierarchicalResources": [
Expand Down
2 changes: 1 addition & 1 deletion packages/acs-client/src/acs/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export function access_controlled_function<T extends ResourceList>(kwargs: {
obligation => obligation.property
);

return appResponse //_.omitDeep(appResponse, property);
return appResponse; // _.omitDeep(appResponse, property);
}
catch (err) {
return {
Expand Down
20 changes: 9 additions & 11 deletions packages/acs-client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const checkSubjectMatch = (user: ResolvedSubject, ruleSubjectAttributes: Attribu
));
logger.debug('Role scoped instances for matching entity', { id: user?.id, ruleRoleScopeEntityName, matchingRoleScopedInstance });
// validate HR scope root ID contains the role scope instances
const hrScopeExist = user?.hierarchical_scopes?.every((hrScope) => matchingRoleScopedInstance.includes(hrScope.id));
const hrScopeExist = user?.hierarchical_scopes?.some((hrScope) => matchingRoleScopedInstance.includes(hrScope.id));
logger.debug('HR Scopes exist', { hrScopeExist });
if (!hrScopeExist) {
logger.info('Hierarchial scopes for matching role does not exist', { role: ruleRoleValue, instances: matchingRoleScopedInstance });
Expand All @@ -121,15 +121,13 @@ const checkSubjectMatch = (user: ResolvedSubject, ruleSubjectAttributes: Attribu
reducedUserScope,
hierarchicalRoleScopingCheck
);
} else {
} else if (hrScopeExist && !user.scope) {
// HR scope match exist but user has not provided scope so still a match is considered
if (!user?.scope) {
logger.debug('Target scope not provided using full HR tree for matched role', { role: ruleRoleValue });
// if no scope is provided then use the complete HR tree for user scopes
user?.hierarchical_scopes?.filter((hrScope) => matchingRoleScopedInstance?.includes(hrScope?.id) && hrScope?.role === ruleRoleValue).forEach((eachHRScope) => {
reduceUserScope(eachHRScope, reducedUserScope, hierarchicalRoleScopingCheck);
});
}
logger.debug('Target scope not provided using full HR tree for matched role', { role: ruleRoleValue });
// if no scope is provided then use the complete HR tree for user scopes
user?.hierarchical_scopes?.filter((hrScope) => matchingRoleScopedInstance?.includes(hrScope?.id) && hrScope?.role === ruleRoleValue).forEach((eachHRScope) => {
reduceUserScope(eachHRScope, reducedUserScope, hierarchicalRoleScopingCheck);
});
return hrScopeExist;
}
} else if (ruleRoleValue) {
Expand Down Expand Up @@ -354,8 +352,8 @@ export const buildFilterPermissions = async (
}
}
else {
subject.hierarchical_scopes ??= [];
subject.role_associations ??= [];
subject.hierarchical_scopes ??=[];
subject.role_associations ??=[];
}

const urns = cfg.get('authorization:urns');
Expand Down
308 changes: 307 additions & 1 deletion packages/acs-client/test/acs.spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,31 @@ const permitRule: RuleRQ = {
effect: Effect.PERMIT
};

const permitRuleUserScope: RuleRQ = {
id: 'permit_rule_id',
target: {
actions: [],
resources: [{
id: 'urn:restorecommerce:acs:names:model:entity',
value: 'urn:test:acs:model:Test.Test'
}],
subjects: [
{
id: 'urn:restorecommerce:acs:names:role',
value: 'user-role'
},
{
id: 'urn:restorecommerce:acs:names:roleScopingEntity',
value: 'urn:test:acs:model:user.User'
},
{
id: 'urn:restorecommerce:acs:names:hierarchicalRoleScoping',
value: 'false'
}]
},
effect: Effect.PERMIT
};

const permitRuleNoHrs: RuleRQ = {
id: 'no_hrs_permit_rule_id',
target: {
Expand Down Expand Up @@ -351,7 +376,7 @@ describe('Testing acs-client', () => {
);
});

it('Should PERMIT creating Test resource with valid user Ctx', async () => {
it('Should PERMIT creating Test resource with valid Org scope matching Ctx', async () => {
// test resource to be created
const resources: CtxResource[] = updateMetaData([{
id: 'test_id',
Expand Down Expand Up @@ -393,6 +418,57 @@ describe('Testing acs-client', () => {
should.equal(response.operation_status?.message, 'success');
});

it('Should PERMIT creating Test resource with valid User scope matching Ctx', async () => {
// test resource to be created
const resources: CtxResource[] = updateMetaData([{
id: 'test_id',
name: 'Test',
description: 'This is a test description',
meta: {
owners: [
{
id: 'urn:restorecommerce:acs:names:ownerIndicatoryEntity',
value: 'urn:test:acs:model:user.User',
attributes: [{
id: 'urn:restorecommerce:acs:names:ownerInstance',
value: 'test_user_id'
}]
}
]
}
}]);

// user ctx data updated in session
const subject = {
id: 'test_user_id',
name: 'test_user',
scope: 'targetScope',
token: 'valid_token',
role_associations: [
{
role: 'user-role'
}
]
};

const ctx: ACSClientContext = {
subject,
resources,
};

// call accessRequest(), the response is from mock ACS
const response = await accessRequest(
subject,
[{ resource: 'Test', id: resources[0].id }],
AuthZAction.CREATE,
ctx
) as DecisionResponse;

should.equal(response.decision, Response_Decision.PERMIT);
should.equal(response.operation_status?.code, 200);
should.equal(response.operation_status?.message, 'success');
});

it('Should DENY reading Test resource (DENY rule)', async () => {
// PolicySet contains DENY rule
PolicySetRQFactory.rules = [denyRule];
Expand Down Expand Up @@ -520,6 +596,236 @@ describe('Testing acs-client', () => {
}
);

it(
'Should DENY reading Test resource (PERMIT rule) with invalid user target scope',
async () => {
// PolicySet contains PERMIT rule
PolicySetRQFactory.rules = [permitRuleUserScope];

// test resource to be read of type 'ReadRequest'
const resources: CtxResource[] = [{
id: 'test_id',
meta: {
owners: []
}
}];

// user ctx data updated in session
const subject = {
id: 'test_user_id',
scope: 'invalidUserId',
token: 'valid_token',
role_associations: [
{
role: 'user-role',
attributes: [
{
id: 'urn:restorecommerce:acs:names:roleScopingEntity',
value: 'urn:test:acs:model:user.User',
attributes: [{
id: 'urn:restorecommerce:acs:names:roleScopingInstance',
value: 'test_user_id'
}]
}
]
}
],
hierarchical_scopes: [{
id: 'test_user_id',
role: 'user-role',
children: []
}]
};

const ctx: ACSClientContext = {
subject,
resources,
};

// call accessRequest(), the response is from mock ACS
const readResponse = await accessRequest(
subject,
[{ resource: 'Test', id: resources[0].id }],
AuthZAction.READ,
ctx, { operation: Operation.whatIsAllowed, database: 'postgres' }
) as PolicySetRQResponse;

should.equal(readResponse.decision, Response_Decision.DENY);
should.equal(readResponse.operation_status?.code, 403);
should.equal(
readResponse.operation_status?.message,
'Access not allowed for request with subject:test_user_id, ' +
'resource:Test, action:READ, target_scope:invalidUserId; the response was DENY'
);
}
);

it(
'Should PERMIT reading Test resource (PERMIT rule User scope) without target scope and verify input filter ' +
'is extended to enforce applicable policies for matching user role',
async () => {
// PolicySet contains PERMIT rule
PolicySetRQFactory.rules = [permitRuleUserScope];

// test resource to be read of type 'ReadRequest'
const resources: CtxResource[] = [{
id: 'test_id',
meta: {
owners: []
}
}];

// user ctx data updated in session
const subject = {
id: 'test_user_id',
// scope: 'targetScope',
token: 'valid_token',
role_associations: [
{
role: 'user-role',
attributes: [
{
id: 'urn:restorecommerce:acs:names:roleScopingEntity',
value: 'urn:test:acs:model:user.User',
attributes: [{
id: 'urn:restorecommerce:acs:names:roleScopingInstance',
value: 'test_user_id'
}]
}
]
}
],
hierarchical_scopes: [{
id: 'test_user_id',
role: 'user-role',
children: []
}]
};

const ctx: ACSClientContext = {
subject,
resources,
};

// call accessRequest(), the response is from mock ACS
const readResponse = await accessRequest(
subject,
[{ resource: 'Test', id: resources[0].id }],
AuthZAction.READ,
ctx, { operation: Operation.whatIsAllowed, database: 'postgres' }
) as PolicySetRQResponse;

should.exist(readResponse.decision);
should.equal(readResponse.decision, Response_Decision.PERMIT);
should.equal(readResponse.operation_status?.code, 200);
should.equal(readResponse.operation_status?.message, 'success');
// verify input is modified to enforce the applicapble poilicies
const filterParamKey = cfg.get('authorization:filterParamKey')[1].value;
const expectedFilterResponse = [{
field: filterParamKey,
operation: 'eq',
value: 'test_user_id'
}];
should.equal(readResponse.filters?.[0]?.resource, 'Test');
const filterEntityMap = readResponse.filters;
const filters = filterEntityMap?.[0].filters;
should.deepEqual(filters?.[0]?.filters?.[0], expectedFilterResponse[0]);
}
);

it(
'Should PERMIT reading Test resource (PERMIT rule Org Scope and User scope) without target scope and ' +
'verify input filter is extended to enforce applicable policies for matching user role',
async () => {
// PolicySet contains PERMIT rule for OrgScoping and UserScoping entity
PolicySetRQFactory.rules = [permitRule, permitRuleUserScope];

// test resource to be read of type 'ReadRequest'
const resources: CtxResource[] = [{
id: 'test_id',
meta: {
owners: []
}
}];

// user ctx data updated in session
const subject = {
id: 'test_user_id',
// scope: 'targetScope',
token: 'valid_token',
role_associations: [
{
role: 'test-role',
attributes: [
{
id: 'urn:restorecommerce:acs:names:roleScopingEntity',
value: 'urn:test:acs:model:organization.Organization',
attributes: [{
id: 'urn:restorecommerce:acs:names:roleScopingInstance',
value: 'targetScope'
}]
}
]
},
{
role: 'user-role',
attributes: [
{
id: 'urn:restorecommerce:acs:names:roleScopingEntity',
value: 'urn:test:acs:model:user.User',
attributes: [{
id: 'urn:restorecommerce:acs:names:roleScopingInstance',
value: 'test_user_id'
}]
}
]
}
],
hierarchical_scopes: [{
id: 'targetScope',
role: 'test-role',
children: [{
id: 'targetSubScope'
}]
}, {
id: 'test_user_id',
role: 'user-role',
children: []
}]
};

const ctx: ACSClientContext = {
subject,
resources,
};

// call accessRequest(), the response is from mock ACS
const readResponse = await accessRequest(
subject,
[{ resource: 'Test', id: resources[0].id }],
AuthZAction.READ,
ctx, { operation: Operation.whatIsAllowed, database: 'postgres' }
) as PolicySetRQResponse;

should.exist(readResponse.decision);
should.equal(readResponse.decision, Response_Decision.PERMIT);
should.equal(readResponse.operation_status?.code, 200);
should.equal(readResponse.operation_status?.message, 'success');
// verify input is modified to enforce the applicapble both Org and User poilicy
const orgFilterKey = cfg.get('authorization:filterParamKey')[0].value;
const userFilterKey = cfg.get('authorization:filterParamKey')[1].value;
const expectedFilterResponse = [
{ field: orgFilterKey, operation: "eq", value: "targetScope" },
{ field: orgFilterKey, operation: "eq", value: "targetSubScope" },
{ field: userFilterKey, operation: "eq", value: "test_user_id" }
];
should.equal(readResponse.filters?.[0]?.resource, 'Test');
const filterEntityMap = readResponse.filters;
const filters = filterEntityMap?.[0].filters;
should.deepEqual(filters?.[0]?.filters?.[0], expectedFilterResponse[0]);
}
);

it(
'Should PERMIT reading Test resource (PERMIT rule) with HR scoping enabled and verify input filter ' +
'is extended to enforce applicable policies',
Expand Down
Loading

0 comments on commit 89319a4

Please sign in to comment.