Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Acs make scope optional #322

Merged
merged 9 commits into from
Apr 15, 2024
17 changes: 3 additions & 14 deletions docs/modules/ROOT/pages/abac.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,7 @@ as demanding such evaluation would require a replication of this functionality a
- id ex: urn:oasis:names:tc:xacml:1.0:subject:subject-id
- value ex: <subject identifier>

# To identify role scoping entity
- id ex: urn:restorecommerce:acs:names:roleScopingEntity
- value ex: urn:restorecommerce:acs:model:organization.Organization

# To identify role scoping instance
# To identify role scoping instance (optional)
- id ex: urn:restorecommerce:acs:names:roleScopeInstance
value: ex: <organization identifier>
- resources
Expand Down Expand Up @@ -190,10 +186,6 @@ request:
subjects:
- id: ex: urn:oasis:names:tc:xacml:1.0:subject:subject-id
value: Alice
- id: urn:restorecommerce:acs:names:roleScopingEntity
value: urn:restorecommerce:acs:model:organization.Organization
- id: urn:restorecommerce:acs:names:roleScopeInstance
value: OrgB
resources:
- id: urn:restorecommerce:acs:names:model:entity
value: urn:restorecommerce:model:device.Device
Expand Down Expand Up @@ -283,7 +275,8 @@ which according to the policy's combining algorithm means access should be grant

The operation `whatIsAllowed` is used when there is not a specific target resource for a request, for example, when Subject aims to see as much resources as possible.
This example illustrates permissible actions on two resource entities `Address` and `Country` for Subject `Alice` who has the role `admin` within the scoping entity
`Organization` with ID 'OrgA'.
`Organization` with ID 'OrgA'. The target role scoping instance in subjects below `OrgA` is optional for `whatIsAllowed`, if it is provided then filters are created by https://github.com/restorecommerce/libs/tree/next/packages/acs-client[`acs-client`] based on
this target role scope instance if not all applicable filters are returned from `acs-client`

[source,yml]
----
Expand All @@ -292,8 +285,6 @@ request:
subjects:
- id: ex: urn:oasis:names:tc:xacml:1.0:subject:subject-id
value: Alice
- id: urn:restorecommerce:acs:names:roleScopingEntity
value: urn:restorecommerce:acs:model:organization.Organization
- id: urn:restorecommerce:acs:names:roleScopeInstance
value: OrgA
resources:
Expand Down Expand Up @@ -394,8 +385,6 @@ request:
subjects:
- id: ex: urn:oasis:names:tc:xacml:1.0:subject:subject-id
value: Alice
- id: urn:restorecommerce:acs:names:roleScopingEntity
value: urn:restorecommerce:acs:model:organization.Organization
- id: urn:restorecommerce:acs:names:roleScopeInstance
value: OrgA
resources:
Expand Down
162 changes: 31 additions & 131 deletions src/core/accessController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ export class AccessController {
) {
const rules: Map<string, Rule> = policy.combinables;
this.logger.verbose(`Checking policy ${policy.name}`);
let policySubjectMatch: boolean;
// Subject set on Policy validate HR scope matching
if (policy?.target?.subjects?.length > 0) {
this.logger.verbose(`Checking Policy subject HR Scope match for ${policy.name}`);
policySubjectMatch = await checkHierarchicalScope(policy.target, request, this.urns, this, this.logger);
} else {
policySubjectMatch = true;
}
// only apply a policy effect if there are no rules
// combine rules otherwise
if (rules.size == 0 && !!policy.effect) {
Expand Down Expand Up @@ -259,7 +267,7 @@ export class AccessController {
matches = await verifyACLList(rule.target, request, this.urns, this, this.logger);
}

if (matches) {
if (matches && policySubjectMatch) {
if (!evaluationCacheableRule) {
evaluation_cacheable = evaluationCacheableRule;
}
Expand Down Expand Up @@ -764,151 +772,43 @@ export class AccessController {
}

/**
* Check if the attributes of subject from a rule, policy
* or policy set match the attributes from a request.
* Check if the Rule's Subject Role matches with atleast
* one of the user role associations role value
*
* @param ruleAttributes
* @param requestSubAttributes
* @param request
*/
private async checkSubjectMatches(ruleSubAttributes: Attribute[],
requestSubAttributes: Attribute[], request: Request): Promise<boolean> {
// 1) Check if the rule subject entity exists, if so then check
// request->target->subject->orgInst or roleScopeInst matches with
// context->subject->role_associations->roleScopeInst or hierarchical_scope
// 2) if 1 is true then subject match is considered
// 3) If rule subject entity does not exist (as for master data resources)
// then check context->subject->role_associations->role against
// Rule->subject->role
const scopingEntityURN = this.urns.get('roleScopingEntity'); // urn:restorecommerce:acs:names:roleScopingEntity
const scopingInstanceURN = this.urns.get('roleScopingInstance'); // urn:restorecommerce:acs:names:roleScopingInstance
const hierarchicalRoleScopingURN = this.urns.get('hierarchicalRoleScoping');
// Just check the Role value matches here in subject
const roleURN = this.urns.get('role');
let matches = false;
let scopingEntExists = false;
let ruleRole;
// default if hierarchicalRoleScopingURN is not configured then consider
// to match the HR scopes
let hierarchicalRoleScoping = 'true';
if (ruleSubAttributes?.length === 0) {
matches = true;
return matches;
let ruleRole: string;
if (!ruleSubAttributes || ruleSubAttributes.length === 0) {
return true;
}
for (let ruleSubAttribute of ruleSubAttributes || []) {
if (ruleSubAttribute?.id === scopingEntityURN) {
// match the scoping entity value
scopingEntExists = true;
for (let requestSubAttribute of requestSubAttributes || []) {
if (requestSubAttribute?.value === ruleSubAttribute?.value) {
matches = true;
break;
}
}
} else if (ruleSubAttribute?.id === roleURN) {
ruleRole = ruleSubAttribute.value;
} else if (ruleSubAttribute?.id === hierarchicalRoleScopingURN) {
hierarchicalRoleScoping = ruleSubAttribute.value;
ruleSubAttributes?.forEach((subjectObject) => {
if (subjectObject?.id === roleURN) {
ruleRole = subjectObject?.value;
}
}
});

let context = (request as any)?.context as ContextWithSubResolved;
// check if context subject_id contains HR scope if not make request 'createHierarchicalScopes'
if (context?.subject?.token &&
_.isEmpty(context.subject.hierarchical_scopes)) {
context = await this.createHRScope(context);
// must be a rule subject targetted to specific user
if (!ruleRole && this.attributesMatch(ruleSubAttributes, requestSubAttributes)) {
this.logger.debug('Rule subject targetted to specific user', ruleSubAttributes);
return true;
}

if (scopingEntExists && matches) {
matches = false;
// check the target scoping instance is present in
// the context subject roleassociations and then update matches to true
if (context?.subject?.role_associations) {
let targetScopingInstance;
requestSubAttributes?.find((obj) => {
if (obj?.id === scopingEntityURN && obj?.attributes?.length > 0) {
obj?.attributes?.filter((roleScopeInstObj) => {
if (roleScopeInstObj?.id === scopingInstanceURN) {
targetScopingInstance = roleScopeInstObj?.value;
}
});
}
});
// check in role_associations
const userRoleAssocs = context?.subject?.role_associations;
if (userRoleAssocs?.length > 0) {
for (let role of userRoleAssocs) {
const roleID = role?.role;
for (let obj of role.attributes || []) {
if (obj?.id === scopingEntityURN && obj?.attributes?.length > 0) {
for (let ruleScopInstObj of obj.attributes) {
if (ruleScopInstObj?.id == scopingInstanceURN && ruleScopInstObj?.value == targetScopingInstance) {
if (!ruleRole || (ruleRole && ruleRole === roleID)) {
matches = true;
return matches;
}
}
}
}
}
}
}
if (!matches && hierarchicalRoleScoping && hierarchicalRoleScoping === 'true') {
// check for HR scope
const hrScopes = context?.subject?.hierarchical_scopes;
if (!hrScopes || hrScopes?.length === 0) {
return matches;
}
for (let hrScope of hrScopes || []) {
if (this.checkTargetInstanceExists(hrScope, targetScopingInstance)) {
const userRoleAssocs = context?.subject?.role_associations;
if (!_.isEmpty(userRoleAssocs)) {
for (let role of userRoleAssocs || []) {
const roleID = role.role;
if (!ruleRole || (ruleRole && ruleRole === roleID)) {
matches = true;
return matches;
}
}
}
}
}
}
}
} else if (!scopingEntExists) {
// scoping entity does not exist - check for point 3.
if (context?.subject) {
const userRoleAssocs = context?.subject?.role_associations;
if (userRoleAssocs?.length > 0) {
const ruleSubAttributeObj = ruleSubAttributes?.find((obj) => obj.id === roleURN);
for (let obj of userRoleAssocs) {
if (obj?.role === ruleSubAttributeObj?.value) {
matches = true;
return matches;
}
}
}
}
// must be a rule subject targetted to specific user
if (!matches && this.attributesMatch(ruleSubAttributes, requestSubAttributes)) {
return true;
}
if (!ruleRole) {
this.logger.warn(`Subject does not match with rule attributes`, ruleSubAttributes);
return false;
}
return matches;
}

private checkTargetInstanceExists(hrScope: HierarchicalScope,
targetScopingInstance: string): boolean {
if (hrScope?.id === targetScopingInstance) {
return true;
} else {
if (hrScope?.children?.length > 0) {
for (let child of hrScope.children) {
if (this.checkTargetInstanceExists(child, targetScopingInstance)) {
return true;
}
}
}
const context = (request as any)?.context as ContextWithSubResolved;
if (!context?.subject?.role_associations) {
this.logger.warn('Subject role associations missing', ruleSubAttributes);
return false;
}
return context?.subject?.role_associations?.some((roleObj) => roleObj?.role === ruleRole);
}

/**
Expand Down