Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Commit

Permalink
fix: handle include/revInclude correctly (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
rsmayda authored Sep 12, 2022
1 parent 1fde707 commit 203bbc0
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 57 deletions.
2 changes: 1 addition & 1 deletion src/smartAuthorizationHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ describe('introspectJwtToken', () => {
Math.floor(Date.now() / 1000),
Math.floor(Date.now() / 1000) + 10,
expectedAudValue,
`${expectedIssValue}/`,
`${expectedIssValue}`,
);
const mock = new MockAdapter(axios);
mock.onPost(introspectUrl).reply(200, {
Expand Down
32 changes: 24 additions & 8 deletions src/smartAuthorizationHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { decode, verify } from 'jsonwebtoken';
import axios from 'axios';
import resourceReferencesMatrixV4 from './schema/fhirResourceReferencesMatrix.v4.0.1.json';
import resourceReferencesMatrixV3 from './schema/fhirResourceReferencesMatrix.v3.0.1.json';
import { FhirResource, IntrospectionOptions } from './smartConfig';
import { AccessModifier, FhirResource, IntrospectionOptions } from './smartConfig';
import { convertScopeToSmartScope } from './smartScopeHelper';

import getComponentLogger from './loggerBuilder';

export const FHIR_USER_REGEX =
Expand Down Expand Up @@ -116,14 +118,27 @@ export function isFhirUserAdmin(fhirUser: FhirResource, adminAccessTypes: string
}

/**
* @param usableScopes this should be usableScope set from the `verifyAccessToken` method
* @param resourceType the type of the resource we are trying to access
* @param scopes: this should be scope set from the `verifyAccessToken` method
* @param resourceType: the type of the resource the request is trying to access
* @param accessModifier: the type of access the request is asking for
* @returns if there is a usable system scope for this request
*/
export function hasSystemAccess(usableScopes: string[], resourceType: string): boolean {
return usableScopes.some(
(scope: string) => scope.startsWith('system/*') || scope.startsWith(`system/${resourceType}`),
);
export function hasSystemAccess(scopes: string[], resourceType: string, accessModifier: AccessModifier): boolean {
return scopes.some((scope: string) => {
try {
const clinicalSmartScope = convertScopeToSmartScope(scope);

return (
clinicalSmartScope.scopeType === 'system' &&
(clinicalSmartScope.resourceType === '*' || clinicalSmartScope.resourceType === resourceType) &&
(clinicalSmartScope.accessType === '*' || clinicalSmartScope.accessType === accessModifier)
);
} catch (e) {
// Error occurs from `convertScopeToSmartScope` if scope was invalid
logger.debug((e as any).message);
return false;
}
});
}

export function hasAccessToResource(
Expand All @@ -134,9 +149,10 @@ export function hasAccessToResource(
adminAccessTypes: string[],
apiUrl: string,
fhirVersion: FhirVersion,
accessModifier: AccessModifier,
): boolean {
return (
hasSystemAccess(usableScopes, sourceResource.resourceType) ||
hasSystemAccess(usableScopes, sourceResource.resourceType, accessModifier) ||
(fhirUserObject &&
(isFhirUserAdmin(fhirUserObject, adminAccessTypes, apiUrl) ||
hasReferenceToResource(fhirUserObject, sourceResource, apiUrl, fhirVersion))) ||
Expand Down
159 changes: 143 additions & 16 deletions src/smartHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
BASE_R4_RESOURCES,
AuthorizationBundleRequest,
GetSearchFilterBasedOnIdentityRequest,
BASE_STU3_RESOURCES,
} from 'fhir-works-on-aws-interface';

import * as smartAuthorizationHelper from './smartAuthorizationHelper';
Expand Down Expand Up @@ -945,6 +946,12 @@ describe('authorizeAndFilterReadResponse', () => {
total: 3,
};

const searchFilteredEntitiesMatch = {
...emptySearchResult,
entry: [createEntry(validPatientObservation), createEntry(validPatientEncounter)],
total: 2,
};

const searchSomeEntitiesMatch = {
...emptySearchResult,
entry: [
Expand Down Expand Up @@ -1212,20 +1219,79 @@ describe('authorizeAndFilterReadResponse', () => {
searchAllEntitiesMatch,
],
[
'SEARCH: system scope; Practitioner able to search and get ALL results',
'SEARCH: system scope; System able to search and get ALL results',
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['system/*.read'],
usableScopes: ['system/*.read'],
fhirUserObject: practitionerFhirResource,
},
operation: 'search-type',
readResponse: searchAllEntitiesMatch,
},
true,
searchAllEntitiesMatch,
],
[
'SEARCH: user scope; filter Patient Resource based on scopes; /Observation? search',
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['user/Observation.read', 'user/Encounter.read'],
usableScopes: ['user/Observation.read'],
fhirUserObject: practitionerFhirResource,
},
operation: 'search-type',
readResponse: searchAllEntitiesMatch,
},
true,
searchFilteredEntitiesMatch,
],
[
'SEARCH: patient scope; filter Patient Resource based on scopes; /Observation? search',
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['patient/Observation.read', 'patient/Encounter.read'],
usableScopes: ['patient/Observation.read'],
patientLaunchContext: patientFhirResource,
},
operation: 'search-type',
readResponse: searchAllEntitiesMatch,
},
true,
searchFilteredEntitiesMatch,
],
[
'SEARCH: system scope; filter Patient Resource based on scopes; /Observation? search',
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['system/Observation.read', 'system/Encounter.read'],
usableScopes: ['system/Observation.read'],
},
operation: 'search-type',
readResponse: searchAllEntitiesMatch,
},
true,
searchFilteredEntitiesMatch,
],
[
'SEARCH: Invalid search result',
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['user/*.read', 'patient/*.read'],
usableScopes: ['user/*.read', 'patient/*.read'],
fhirUserObject: patientFhirResource,
patientLaunchContext: patientFhirResource,
},
operation: 'search-system',
readResponse: {},
},
true,
{ entry: [], total: 0 },
],
];

const authZHandler: SMARTHandler = new SMARTHandler(baseAuthZConfig(), apiUrl, '4.0.1');
Expand Down Expand Up @@ -1265,7 +1331,6 @@ describe('isWriteRequestAuthorized', () => {
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['patient/*.write'],
usableScopes: ['patient/*.write'],
patientLaunchContext: patientFhirResource,
},
Expand All @@ -1279,7 +1344,6 @@ describe('isWriteRequestAuthorized', () => {
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['user/*.write'],
usableScopes: ['user/*.write'],
fhirUserObject: practitionerFhirResource,
},
Expand All @@ -1293,7 +1357,6 @@ describe('isWriteRequestAuthorized', () => {
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['user/*.write'],
usableScopes: ['user/*.write'],
fhirUserObject: externalPractitionerFhirResource,
},
Expand All @@ -1303,11 +1366,10 @@ describe('isWriteRequestAuthorized', () => {
true,
],
[
'PATCH: missing user/patient context; but has system scopes',
'PATCH: has system scopes',
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['user/*.write', 'patient/*.write', 'system/*.write'],
usableScopes: ['system/*.write'],
},
operation: 'patch',
Expand All @@ -1316,11 +1378,10 @@ describe('isWriteRequestAuthorized', () => {
true,
],
[
'PATCH: missing user/patient context',
'PATCH: no usable scope',
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['user/*.write', 'patient/*.write'],
usableScopes: [],
},
operation: 'patch',
Expand All @@ -1333,7 +1394,6 @@ describe('isWriteRequestAuthorized', () => {
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['user/*.write'],
usableScopes: ['user/*.write'],
fhirUserObject: externalPractitionerFhirResource,
},
Expand All @@ -1347,7 +1407,6 @@ describe('isWriteRequestAuthorized', () => {
{
userIdentity: {
...baseAccessNoScopes,
scopes: ['user/*.write', 'system/*.write'],
usableScopes: ['user/*.write', 'system/*.write'],
fhirUserObject: externalPractitionerFhirResource,
},
Expand All @@ -1356,6 +1415,44 @@ describe('isWriteRequestAuthorized', () => {
},
true,
],
[
'UPDATE: specific scope',
{
userIdentity: {
...baseAccessNoScopes,
usableScopes: ['system/Patient.write'],
},
operation: 'update',
resourceBody: validPatient,
},
true,
],
[
'CREATE: specific scope',
{
userIdentity: {
...baseAccessNoScopes,
usableScopes: ['user/Patient.write'],
fhirUserObject: practitionerFhirResource,
},
operation: 'create',
resourceBody: validPatient,
},
true,
],
[
'PATCH: specific scope',
{
userIdentity: {
...baseAccessNoScopes,
usableScopes: ['patient/Encounter.write'],
patientLaunchContext: patientFhirResource,
},
operation: 'patch',
resourceBody: validPatientEncounter,
},
true,
],
];

const authZHandler: SMARTHandler = new SMARTHandler(baseAuthZConfig(), apiUrl, '4.0.1');
Expand Down Expand Up @@ -1436,7 +1533,7 @@ describe('isAccessBulkDataJobAllowed', () => {
});
});

describe('isBundleRequestAuthorized', () => {
describe('isBundleRequestAuthorized; write an Observation; read a Patient; search-type Encounter', () => {
const authZConfigWithSearchTypeScope = baseAuthZConfig();
authZConfigWithSearchTypeScope.scopeRule.user.write = ['create'];
const authZHandler: SMARTHandler = new SMARTHandler(authZConfigWithSearchTypeScope, apiUrl, '4.0.1');
Expand Down Expand Up @@ -1472,7 +1569,7 @@ describe('isBundleRequestAuthorized', () => {
'practitioner with patient&user scope: no error',
{
...baseAccessNoScopes,
scopes: ['user/Observation.write', 'patient/Patient.*', 'openid'],
scopes: ['user/Observation.write', 'patient/Patient.*', 'patient/Encounter.read', 'openid'],
fhirUserObject: practitionerFhirResource,
patientLaunchContext: patientFhirResource,
},
Expand All @@ -1482,11 +1579,20 @@ describe('isBundleRequestAuthorized', () => {
'practitioner with system&user scope: no error',
{
...baseAccessNoScopes,
scopes: ['user/Observation.write', 'system/Patient.*', 'openid'],
scopes: ['user/Observation.write', 'system/Patient.*', 'user/Encounter.read', 'openid'],
fhirUserObject: practitionerFhirResource,
},
true,
],
[
'practitioner with system&user scope; but no Encounter scope: Unauthorized Error',
{
...baseAccessNoScopes,
scopes: ['user/Observation.write', 'system/Patient.*', 'openid'],
fhirUserObject: practitionerFhirResource,
},
false,
],
[
'patient with user scope to create observation but no read perms: Unauthorized Error',
{
Expand Down Expand Up @@ -1544,6 +1650,13 @@ describe('isBundleRequestAuthorized', () => {
resourceType: 'Patient',
id: '160265f7-e8c2-45ca-a1bc-317399e23548',
},
{
operation: 'search-type',
resource: '/Encounter?_include=Patient',
fullUrl: '/Encounter?_include=Patient',
resourceType: 'Encounter',
id: '',
},
],
};

Expand All @@ -1564,8 +1677,8 @@ describe('getAllowedResourceTypesForOperation', () => {

const cases: (string | string[])[][] = [
[
'search-type operation: read scope: returns [Patient, Observation]',
['user/Patient.read', 'user/Observation.read', 'fhirUser', 'system/Encounter.*'],
'search-type operation: read scope: returns [Patient, Observation, Encounter] not Fake though',
['user/Patient.read', 'user/Observation.read', 'fhirUser', 'system/Encounter.*', 'system/Fake.*'],
'search-type',
['Patient', 'Observation', 'Encounter'],
],
Expand Down Expand Up @@ -1608,6 +1721,20 @@ describe('getAllowedResourceTypesForOperation', () => {
expectedAllowedResources,
);
});
test('ver3_CASE: search-type operation: read scope: returns [BASE_R3_RESOURCES]', async () => {
const authZHandlerVer3: SMARTHandler = new SMARTHandler(authZConfigWithSearchTypeScope, apiUrl, '3.0.1');

const request: AllowedResourceTypesForOperationRequest = {
userIdentity: {
scopes: ['user/*.read'],
},
operation: 'search-type',
};

await expect(authZHandlerVer3.getAllowedResourceTypesForOperation(request)).resolves.toEqual(
BASE_STU3_RESOURCES,
);
});
});

describe('getSearchFilterBasedOnIdentity', () => {
Expand Down
Loading

0 comments on commit 203bbc0

Please sign in to comment.