Skip to content

Commit

Permalink
Merge branch 'acs-client-scope-optional' into next
Browse files Browse the repository at this point in the history
  • Loading branch information
Arun-KumarH committed Apr 15, 2024
2 parents b84841e + 6a30d32 commit ec419b7
Show file tree
Hide file tree
Showing 11 changed files with 487 additions and 298 deletions.
33 changes: 14 additions & 19 deletions packages/acs-client/src/acs/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const createActionTarget = (action: any): Attribute[] => {
}
};

export const createSubjectTarget = (subject: DeepPartial<Subject>, orgScopeURN): Attribute[] => {
export const createSubjectTarget = (subject: DeepPartial<Subject>): Attribute[] => {
if (subject.unauthenticated) {
return [{
id: urns.unauthenticated_user,
Expand All @@ -79,16 +79,11 @@ export const createSubjectTarget = (subject: DeepPartial<Subject>, orgScopeURN):
];

if (subject.scope) {
orgScopeURN = orgScopeURN ? orgScopeURN : 'urn:restorecommerce:acs:model:organization.Organization';
flattened = flattened.concat([
{
id: urns.roleScopingEntity,
value: orgScopeURN,
attributes: [{
id: urns.roleScopingInstance,
value: subject.scope,
attributes: []
}]
id: urns.roleScopingInstance,
value: subject.scope,
attributes: []
}
]);
}
Expand Down Expand Up @@ -199,11 +194,11 @@ export class UnAuthZ implements IAuthZ {
}

async isAllowed(request: Request<NoAuthTarget, AuthZContext>,
ctx: ACSClientContext, useCache: boolean, roleScopingEntityURN: string): Promise<DecisionResponse> {
ctx: ACSClientContext, useCache: boolean): Promise<DecisionResponse> {
const authZRequest = {
target: {
actions: createActionTarget(request.target.actions),
subjects: createSubjectTarget(request.target.subjects, roleScopingEntityURN),
subjects: createSubjectTarget(request.target.subjects),
resources: createResourceTarget(request.target.resources, request.target.actions)
},
context: {
Expand Down Expand Up @@ -246,11 +241,11 @@ export class UnAuthZ implements IAuthZ {
}

async whatIsAllowed(request: Request<NoAuthWhatIsAllowedTarget, AuthZContext>,
ctx: ACSClientContext, useCache: boolean, roleScopingEntityURN: string): Promise<PolicySetRQResponse> {
ctx: ACSClientContext, useCache: boolean): Promise<PolicySetRQResponse> {
const authZRequest = {
target: {
actions: createActionTarget(request.target.actions),
subjects: createSubjectTarget(request.target.subjects, roleScopingEntityURN),
subjects: createSubjectTarget(request.target.subjects),
resources: createResourceTarget(request.target.resources, request.target.actions)
},
context: {
Expand Down Expand Up @@ -309,8 +304,8 @@ export class ACSAuthZ implements IAuthZ {
* @param useCache
* @returns {DecisionResponse}
*/
async isAllowed(request: Request<AuthZTarget, AuthZContext>, ctx: ACSClientContext, useCache, roleScopingEntityURN: string): Promise<DecisionResponse> {
const authZRequest = this.prepareRequest(request, roleScopingEntityURN);
async isAllowed(request: Request<AuthZTarget, AuthZContext>, ctx: ACSClientContext, useCache): Promise<DecisionResponse> {
const authZRequest = this.prepareRequest(request);
authZRequest.context = {
subject: {},
resources: [],
Expand Down Expand Up @@ -370,8 +365,8 @@ export class ACSAuthZ implements IAuthZ {
* @param resource
*/
async whatIsAllowed(request: Request<AuthZWhatIsAllowedTarget, AuthZContext>,
ctx: ACSClientContext, useCache: boolean, roleScopingEntityURN: string): Promise<PolicySetRQResponse> {
const authZRequest = this.prepareRequest(request, roleScopingEntityURN);
ctx: ACSClientContext, useCache: boolean): Promise<PolicySetRQResponse> {
const authZRequest = this.prepareRequest(request);
authZRequest.context = {
subject: {},
resources: [],
Expand Down Expand Up @@ -431,12 +426,12 @@ export class ACSAuthZ implements IAuthZ {
}
}

prepareRequest(request: Request<AuthZTarget | AuthZWhatIsAllowedTarget, AuthZContext>, roleScopingEntityURN): any {
prepareRequest(request: Request<AuthZTarget | AuthZWhatIsAllowedTarget, AuthZContext>): any {
let { subjects, resources, actions } = request.target;
const authZRequest: any = {
target: {
actions: createActionTarget(actions),
subjects: createSubjectTarget(subjects, roleScopingEntityURN),
subjects: createSubjectTarget(subjects),
},
};
authZRequest.target.resources = createResourceTarget(resources, actions);
Expand Down
1 change: 0 additions & 1 deletion packages/acs-client/src/acs/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,5 +246,4 @@ export interface ACSClientOptions {
operation?: Operation;
database?: 'arangoDB' | 'postgres';
useCache?: boolean; // default value is true
roleScopingEntityURN?: string; // default value is Organization
}
30 changes: 15 additions & 15 deletions packages/acs-client/src/acs/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const subjectIsUnauthenticated = (subject: any): subject is UnauthenticatedConte
};

const whatIsAllowedRequest = async (subject: DeepPartial<Subject>, resources: Resource[],
actions: AuthZAction, ctx: ACSClientContext, useCache: boolean, roleScopingEntityURN: string) => {
actions: AuthZAction, ctx: ACSClientContext, useCache: boolean) => {
if (subjectIsUnauthenticated(subject)) {
return await unauthZ.whatIsAllowed({
target: {
Expand All @@ -50,7 +50,7 @@ const whatIsAllowedRequest = async (subject: DeepPartial<Subject>, resources: Re
context: {
security: {}
}
}, ctx, useCache, roleScopingEntityURN);
}, ctx, useCache);
} else {
return await authZ.whatIsAllowed({
context: {
Expand All @@ -61,12 +61,12 @@ const whatIsAllowedRequest = async (subject: DeepPartial<Subject>, resources: Re
resources,
actions
}
}, ctx, useCache, roleScopingEntityURN);
}, ctx, useCache);
}
};

export const isAllowedRequest = async (subject: Subject,
resources: Resource[], actions: AuthZAction, ctx: ACSClientContext, useCache: boolean, roleScopingEntityURN: string): Promise<DecisionResponse> => {
resources: Resource[], actions: AuthZAction, ctx: ACSClientContext, useCache: boolean): Promise<DecisionResponse> => {
if (subjectIsUnauthenticated(subject)) {
return await unauthZ.isAllowed({
target: {
Expand All @@ -75,7 +75,7 @@ export const isAllowedRequest = async (subject: Subject,
context: {
security: {}
}
}, ctx, useCache, roleScopingEntityURN);
}, ctx, useCache);
} else {
return await authZ.isAllowed({
context: {
Expand All @@ -86,7 +86,7 @@ export const isAllowedRequest = async (subject: Subject,
resources,
actions
}
}, ctx, useCache, roleScopingEntityURN);
}, ctx, useCache);
}
};

Expand Down Expand Up @@ -161,6 +161,7 @@ export const accessRequest = async (
// resolve userID by token
const subjectID = subject?.id;
const targetScope = subject?.scope;
const targetScopeMessage = targetScope ? `, target_scope:${ targetScope };` : ';';
if (resource && !_.isArray(resource)) {
resource = [resource];
}
Expand All @@ -169,7 +170,7 @@ export const accessRequest = async (
if (_.isEmpty(resource)) {
const msg = [
`Access not allowed for request with`,
`subject:${ subjectID }, resource:${ resourceName }, action:${ action }, target_scope:${ targetScope };`,
`subject:${ subjectID }, resource:${ resourceName }, action:${ action }${targetScopeMessage}`,
`the response was ${ Response_Decision.INDETERMINATE }`,
].join(' ');
const details = 'Entity missing';
Expand All @@ -189,8 +190,6 @@ export const accessRequest = async (
// default database is arangoDB
const database = options?.database ? options.database : 'arangoDB';
const useCache = options?.useCache ? options.useCache : true;
// default value is RC organization
const roleScopingEntityURN = options?.roleScopingEntityURN ? options.roleScopingEntityURN: 'urn:restorecommerce:acs:model:organization.Organization';
// ctx.resources
if (ctx.resources && !_.isArray(ctx.resources)) {
ctx.resources = [ctx.resources];
Expand All @@ -207,7 +206,7 @@ export const accessRequest = async (
resource,
action,
ctx,
useCache, roleScopingEntityURN
useCache
);
} catch (err) {
logger.error(
Expand All @@ -228,7 +227,7 @@ export const accessRequest = async (
if (authzEnforced && (!policySetResponse || _.isEmpty(policySetResponse.policy_sets))) {
const msg = [
`Access not allowed for request with subject:${ subjectID },`,
`resource:${ resourceName }, action:${ action }, target_scope:${ targetScope };`,
`resource:${ resourceName }, action:${ action }${targetScopeMessage}`,
'the response was INDETERMINATE'
].join(' ');
const details = 'no matching policy/rule could be found';
Expand All @@ -246,13 +245,14 @@ export const accessRequest = async (
if (!authzEnforced && (!policySetResponse || _.isEmpty(policySetResponse.policy_sets))) {
logger.verbose([
`The Access response was INDETERMIATE for a request with subject:${ subjectID },`,
`resource:${ resourceName }, action:${ action }, target_scope:${ targetScope }`,
`resource:${ resourceName }, action:${ action }${targetScopeMessage}`,
`as no matching policy/rule could be found, but since ACS enforcement`,
`config is disabled overriding the ACS result`,
].join(' '));
}

// create filters to enforce applicable policies and custom query / args if applicable
// TODO check and modify this
const resourceFilters = await createResourceFilterMap(resource, policySetResponse,
ctx.resources, action, subClone, subjectID, authzEnforced, targetScope, database);

Expand All @@ -273,7 +273,7 @@ export const accessRequest = async (
if (operation === Operation.isAllowed) {
// authorization
try {
decisionResponse = await isAllowedRequest(subClone as Subject, resource, action, ctx, useCache, roleScopingEntityURN);
decisionResponse = await isAllowedRequest(subClone as Subject, resource, action, ctx, useCache);
} catch (err) {
logger.error('Error calling isAllowed operation', { code: err.code, message: err.message, stack: err.stack });
return { decision: Response_Decision.DENY, operation_status: generateOperationStatus(err.code, err.message) };
Expand All @@ -288,7 +288,7 @@ export const accessRequest = async (
}
const msg = [
`Access not allowed for request with subject:${ subjectID },`,
`resource:${ resourceName }, action:${ action }, target_scope:${ targetScope };`,
`resource:${ resourceName }, action:${ action }${targetScopeMessage}`,
`the response was ${Response_Decision[decisionResponse.decision]}`,
].join(' ');
logger.verbose(msg);
Expand All @@ -309,7 +309,7 @@ export const accessRequest = async (
}
logger.verbose([
`Access not allowed for request with subject:${ subjectID },`,
`resource:${ resourceName }, action:${ action }, target_scope:${ targetScope };`,
`resource:${ resourceName }, action:${ action }${targetScopeMessage}`,
`the response was ${Response_Decision[decisionResponse.decision]}`,
]).join(' ');
logger.verbose(`${details}, Overriding the ACS result as ACS enforce config is disabled`);
Expand Down
112 changes: 52 additions & 60 deletions packages/acs-client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,23 @@ const reduceUserScope = (hrScope: HierarchicalScope, reducedUserScope: string[],
}
};

const checkTargetScopeExists = (hrScope: HierarchicalScope, targetScope: string,
const checkTargetScopeExists = (hrScopes: HierarchicalScope[], targetScope: string,
reducedUserScope: string[], hierarchicalRoleScopingCheck: string): boolean => {
if (hrScope?.id === targetScope) {
// found the target scope object, iterate and put the orgs in reducedUserScope array
logger.debug(`Target entity match found in the user's hierarchical scope`);
reduceUserScope(hrScope, reducedUserScope, hierarchicalRoleScopingCheck);
return true;
} else if (hrScope?.children?.length > 0 && hierarchicalRoleScopingCheck === 'true') {
for (let childNode of hrScope.children) {
if (checkTargetScopeExists(childNode, targetScope, reducedUserScope, hierarchicalRoleScopingCheck)) {
return true;
return hrScopes.some((hrScope) => {
if (hrScope?.id === targetScope) {
// found the target scope object, iterate and put the orgs in reducedUserScope array
logger.debug(`Target entity match found in the user's hierarchical scope`);
reduceUserScope(hrScope, reducedUserScope, hierarchicalRoleScopingCheck);
return true;
} else if (hrScope?.children?.length > 0 && hierarchicalRoleScopingCheck === 'true') {
for (let childNode of hrScope.children) {
if (checkTargetScopeExists([childNode], targetScope, reducedUserScope, hierarchicalRoleScopingCheck)) {
return true;
}
}
}
}
return false;
return false;
});
};

const checkSubjectMatch = (user: ResolvedSubject, ruleSubjectAttributes: Attribute[],
Expand All @@ -66,10 +68,7 @@ const checkSubjectMatch = (user: ResolvedSubject, ruleSubjectAttributes: Attribu
// role URN exists
// 2) Now check if the subject rule role value matches with one of the users ctx role_associations
// then get the corresponding scope instance and check if the targetScope is present in user HR scope Object
let roleScopeEntExists = false;
let roleValueExists = false;
// by default HR scoping check is considered
let hierarchicalRoleScopingCheck = 'true';
let hierarchicalRoleScopingCheck = 'true'; // by default HR scoping check is considered
let ruleRoleValue;
let ruleRoleScopeEntityName;
const urns = cfg.get('authorization:urns');
Expand All @@ -81,60 +80,53 @@ const checkSubjectMatch = (user: ResolvedSubject, ruleSubjectAttributes: Attribu
return true;
}
if (attribute?.id === urns.roleScopingEntity) {
roleScopeEntExists = true;
ruleRoleScopeEntityName = attribute.value;
} else if (attribute.id === urns.role) { // urns.role -> urn:restorecommerce:acs:names:role
roleValueExists = true;
ruleRoleValue = attribute.value;
} else if (attribute?.id === urns.hierarchicalRoleScoping) {
hierarchicalRoleScopingCheck = attribute.value;
}
}

let userAssocScope;
if (roleScopeEntExists && roleValueExists) {
if (user?.role_associations?.length > 0) {
for (let role of user?.role_associations) {
if (role?.role === ruleRoleValue) {
// check the targetScope exists in the user HR scope object
let roleScopeEntityNameMatched = false;
for (let roleAttrs of role?.attributes) {
// urn:restorecommerce:acs:names:roleScopingInstance
if (roleAttrs?.id === urns.roleScopingEntity &&
roleAttrs?.value === ruleRoleScopeEntityName) {
roleScopeEntityNameMatched = true;
}
if (roleScopeEntityNameMatched && roleAttrs?.attributes?.length > 0) {
for (let roleScopeInstObj of roleAttrs.attributes) {
if (roleScopeInstObj.id === urns.roleScopingInstance) {
userAssocScope = roleScopeInstObj.value;
break;
}
}
if (ruleRoleValue && ruleRoleScopeEntityName) {
const matchingRoleScopedInstance: string[] = user?.role_associations?.filter((roleObj) => {
return roleObj?.attributes?.some((roleAttributeObj) => {
if (roleAttributeObj?.id === urns?.roleScopingEntity
&& roleAttributeObj?.value === ruleRoleScopeEntityName) {
return roleAttributeObj?.attributes?.some((roleScopingInstanceObj) => {
if (roleScopingInstanceObj?.id === urns?.roleScopingInstance) {
return roleScopingInstanceObj?.value;
}
}

// check if this userAssocScope's HR object contains the targetScope
const userAssocHRScope = user?.hierarchical_scopes.find(
hrScope => hrScope?.id === userAssocScope
);

// check HR scope matching for subject if hierarchicalRoleScopingCheck is 'true'
if (!userAssocHRScope) {
return false;
}
else if (checkTargetScopeExists(
userAssocHRScope,
user.scope,
reducedUserScope,
hierarchicalRoleScopingCheck
)) {
return true;
}
});
}
});
}).flatMap((roleObj) => roleObj?.attributes?.map(
roleObjAttr => roleObjAttr?.attributes?.map((attrInstObj) => attrInstObj.value)[0]
));

// validate HR scope root ID contains the role scope instances
const hrScopeExist = user?.hierarchical_scopes?.every((hrScope) => matchingRoleScopedInstance.includes(hrScope.id));
if (!hrScopeExist) {
logger.info('Hierarchial scopes for matching role does not exist', { role: ruleRoleValue, instances: matchingRoleScopedInstance });
return false;
} else if (hrScopeExist && user?.scope) {
return checkTargetScopeExists(
user?.hierarchical_scopes?.filter((hrScope) => matchingRoleScopedInstance?.includes(hrScope?.id)),
user?.scope,
reducedUserScope,
hierarchicalRoleScopingCheck
);
} else {
// HR scope match exist but user has not provided scope so still a match is considered
if (!user?.scope) {
// if no scope is provided then use the complete HR tree for user scopes
user?.hierarchical_scopes?.filter((hrScope) => matchingRoleScopedInstance?.includes(hrScope?.id)).forEach((eachHRScope) => {
reduceUserScope(eachHRScope, reducedUserScope, hierarchicalRoleScopingCheck);
});
}
return hrScopeExist;
}
} else if (roleValueExists) {
} else if (ruleRoleValue) {
return user?.role_associations?.some(
ra => ra.role === ruleRoleValue
);
Expand Down Expand Up @@ -347,8 +339,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
Loading

0 comments on commit ec419b7

Please sign in to comment.