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

fix: handle include/revInclude correctly #92

Merged
merged 2 commits into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}`,
rsmayda marked this conversation as resolved.
Show resolved Hide resolved
);
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
rsmayda marked this conversation as resolved.
Show resolved Hide resolved
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,
],
[
rsmayda marked this conversation as resolved.
Show resolved Hide resolved
'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