Skip to content

Commit

Permalink
Update roledefinition and matching
Browse files Browse the repository at this point in the history
  • Loading branch information
peterMuriuki committed Dec 17, 2024
1 parent b26ab17 commit b7c6d16
Show file tree
Hide file tree
Showing 15 changed files with 131 additions and 91 deletions.
10 changes: 5 additions & 5 deletions app/src/App/fhir-apps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,15 @@ const FHIRApps = () => {
disableLoginProtection={DISABLE_LOGIN_PROTECTION}
exact
path={DATA_IMPORT_LIST_URL}
permissions={['WebDataImport.read']}
permissions={['DataImport.read']}
component={DataImportList}
/>
<PrivateComponent
redirectPath={APP_CALLBACK_URL}
disableLoginProtection={DISABLE_LOGIN_PROTECTION}
exact
path={`${DATA_IMPORT_CREATE_URL}`}
permissions={['WebDataImport.create']}
permissions={['DataImport.create']}
component={StartDataImport}
/>
<PrivateComponent
Expand All @@ -233,7 +233,7 @@ const FHIRApps = () => {
exact
path={`${DATA_IMPORT_DETAIL_URL}/:${'workflowId'}`}
{...patientProps}
permissions={['WebDataImport.read']}
permissions={['DataImport.read']}
component={ImportDetailViewDetails}
/>
<PrivateComponent
Expand All @@ -242,7 +242,7 @@ const FHIRApps = () => {
exact
path={`${DATA_IMPORT_LIST_URL}/:${'workflowId'}`}
{...patientProps}
permissions={['WebDataImport.read']}
permissions={['DataImport.read']}
component={DataImportList}
/>
<PrivateComponent
Expand Down Expand Up @@ -590,7 +590,7 @@ const FHIRApps = () => {
exact
path={APP_LOGIN_URL}
render={() => {
window.location.href = OpenSRP;
window.location.href = `${OpenSRP}&kc_locale=fr`;
return <></>;
}}
/>
Expand Down
3 changes: 3 additions & 0 deletions app/src/configs/dispatchConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ProjectCode,
setAllConfigs,
getAllConfigs,
clientIdConfig,
} from '@opensrp/pkg-config';
import {
BACKEND_ACTIVE,
Expand All @@ -17,6 +18,7 @@ import {
AUTHZ_STRATEGY,
COMMODITIES_LIST_RESOURCE_ID,
FHIR_INVENTORY_LIST_ID,
OPENSRP_CLIENT_ID,
} from './env';
import { URL_BACKEND_LOGIN, URL_REACT_LOGIN } from '../constants';

Expand All @@ -26,6 +28,7 @@ const defaultvalues = getAllConfigs();

const configObject: ConfigState = {
...defaultvalues,
[clientIdConfig]: OPENSRP_CLIENT_ID,
languageCode: LANGUAGE_CODE as LanguageCode,
projectCode: PROJECT_CODE as ProjectCode,
appLoginURL: APP_LOGIN_URL,
Expand Down
2 changes: 1 addition & 1 deletion app/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export function getRoutes(roles: string[], t: TFunction, userRole: UserRole): Ro
title: t('Data Imports'),
key: 'data-import',
enabled: true,
permissions: ['WebDataImport.read'],
permissions: ['DataImport.read'],
url: DATA_IMPORT_LIST_URL,
isHomePageLink: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export const ImportDetailViewDetails = (_: RouteComponentProps) => {
[t('Author')]: data.author,
};

console.log({pageTitle})

Check warning on line 83 in packages/fhir-import/src/containers/ImportDetailView/index.tsx

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Unexpected console statement

Check failure on line 83 in packages/fhir-import/src/containers/ImportDetailView/index.tsx

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Replace `pageTitle})` with `·pageTitle·});`

return (
<BodyLayout headerProps={headerProps}>
<Helmet>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { authenticateUser } from '@onaio/session-reducer';
import { Route, Router, Switch } from 'react-router';
import nock from 'nock';
import * as reactQuery from 'react-query';
import { waitForElementToBeRemoved, render, cleanup } from '@testing-library/react';
import { waitForElementToBeRemoved, render, cleanup, prettyDOM } from '@testing-library/react';
import { store } from '@opensrp/store';
import * as constants from '../../../constants';
import { createMemoryHistory } from 'history';
Expand Down Expand Up @@ -100,10 +100,15 @@ describe('Care Teams list view', () => {
await waitForElementToBeRemoved(document.querySelector('.ant-spin'));
expect(nock.pendingMocks()).toEqual([]);

expect(document.querySelector('title')).toMatchInlineSnapshot(`
<title>
View details | 26aae779-0e6f-482d-82c3-a0fad1fd3689_orgToLocationAssignment
</title>
console.log(prettyDOM(document));

Check warning on line 103 in packages/fhir-import/src/containers/ImportDetailView/tests/index.test.tsx

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Unexpected console statement

expect(document.querySelector('.ant-page-header-heading-title')).toMatchInlineSnapshot(`

Check warning on line 105 in packages/fhir-import/src/containers/ImportDetailView/tests/index.test.tsx

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Expected Jest snapshot to be smaller than 6 lines but was 7 lines long
<span
class="ant-page-header-heading-title"
title="View details | 26aae779-0e6f-482d-82c3-a0fad1fd3689_orgToLocationAssignment"
>
View details | 26aae779-0e6f-482d-82c3-a0fad1fd3689_orgToLocationAssignment
</span>
`);

expect(
Expand Down
4 changes: 2 additions & 2 deletions packages/fhir-import/src/containers/ImportListView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const DataImportList = () => {
// eslint-disable-next-line react/display-name
render: (_: unknown, record: WorkflowDescription) => (
<span className="d-flex align-items-center">
<RbacCheck permissions={['WebDataImport.read']}>
<RbacCheck permissions={['DataImport.read']}>
<>
<Link
to={`${DATA_IMPORT_DETAIL_URL}/${record.workflowId.toString()}`}
Expand Down Expand Up @@ -118,7 +118,7 @@ export const DataImportList = () => {
<Col className="main-content">
<div className="main-content__header">
<div />
<RbacCheck permissions={['WebDataImport.create']}>
<RbacCheck permissions={['DataImport.create']}>
<Link to={DATA_IMPORT_CREATE_URL}>
<Button type="primary" onClick={() => history.push(DATA_IMPORT_CREATE_URL)}>
<CloudUploadOutlined />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ exports[`Care Teams list view renders correctly: table row 1 page 1 6`] = `
>
<span
class="d-flex align-items-center"
/>
>
<a
class="m-0 p-1"
href="/importDetail/26aae779-0e6f-482d-82c3-a0fad1fd3689_orgToLocationAssignment"
>
View
</a>
</span>
</td>
`;
6 changes: 4 additions & 2 deletions packages/pkg-config/src/configStore/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** stores configuration for any other package */
import { createGlobalState } from 'react-hooks-global-state';
import { USER_PREFERENCE_KEY } from '../constants';
import { clientIdConfig, USER_PREFERENCE_KEY } from '../constants';
import { PaginationProps } from 'antd/lib/pagination/Pagination';

export const supportedLanguageCodes = ['en', 'sw', 'fr', 'ar', 'th', 'vi'] as const;
Expand Down Expand Up @@ -31,6 +31,7 @@ export interface TableState {

/** interface for configs for this package */
export interface ConfigState {
[clientIdConfig]?: string;
languageCode?: LanguageCode;
projectCode?: ProjectCode;
appLoginURL?: string;
Expand Down Expand Up @@ -58,7 +59,8 @@ export enum PractToOrgAssignmentStrategy {
ONE_TO_MANY = 'ONE_TO_MANY', // one practitioner assignable to multiple organizations
}

const defaultConfigs: GlobalState = {
const defaultConfigs: Partial<GlobalState> = {
[clientIdConfig]: undefined,
languageCode: 'en',
appLoginURL: undefined,
keycloakBaseURL: undefined,
Expand Down
4 changes: 4 additions & 0 deletions packages/pkg-config/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export const SLICE_NOT_REGISTERED =
'Looks like configuration slice is being used without having been yet registered to the store';
export const USER_PREFERENCE_KEY = 'Preference';


Check failure on line 5 in packages/pkg-config/src/constants.ts

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Delete `⏎`
// magic strings
export const clientIdConfig = "clientId" as const

Check failure on line 7 in packages/pkg-config/src/constants.ts

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Replace `"clientId"·as·const` with `'clientId'·as·const;`
1 change: 1 addition & 0 deletions packages/pkg-config/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './configStore';
export * from './constants';
98 changes: 59 additions & 39 deletions packages/rbac/src/adapters/keycloakAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import {
AuthZResource,
KeycloakDefinedResource,
KeycloakDefinedResources,
Permit,
} from '../constants';
import { AllSupportedRoles, allSupportedRoles, Permit } from '../constants';
import { RbacAdapter } from '../helpers/types';
import { UserRole } from '../roleDefinition';
import { clientIdConfig, getConfig } from '@opensrp/pkg-config';

const fhirVerbToPermitLookup: Record<string, Permit> = {
GET: Permit.READ,
Expand All @@ -15,8 +11,8 @@ const fhirVerbToPermitLookup: Record<string, Permit> = {
MANAGE: Permit.MANAGE,
};

const getFhirResourceString = (rawResourceString: string): KeycloakDefinedResource | undefined => {
const matchedResource = KeycloakDefinedResources.filter(
const getFhirResourceString = (rawResourceString: string): AllSupportedRoles | undefined => {
const matchedResource = allSupportedRoles.filter(
(resource) => resource.toUpperCase() === rawResourceString.toUpperCase()
);
return matchedResource[0];
Expand All @@ -37,24 +33,43 @@ export const parseFHirRoles = (role: string) => {
const permit = fhirVerbToPermitLookup[verb.toUpperCase()];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (resource && permit) {
return new UserRole(resource as AuthZResource, permit);
return new UserRole(resource as AllSupportedRoles, permit);
}
};

const keycloakRoleMappings: Record<string, UserRole> = {
'realm-admin': new UserRole(['iam_group', 'iam_role', 'iam_user'], Permit.MANAGE),
'view-users': new UserRole(['iam_user'], Permit.READ),
'manage-users': UserRole.combineRoles([
new UserRole(['iam_user'], Permit.MANAGE),
new UserRole(['iam_role', 'iam_group'], Permit.READ),
]),
'query-groups': new UserRole(['iam_group'], Permit.READ),
'view-groups': new UserRole(['iam_group'], Permit.READ),
'query-users': new UserRole(['iam_user'], Permit.READ),
};
export const parseKeycloakClientRoles = (scope: string, stringRole: string) => {
const configuredClientId = getConfig(clientIdConfig) ?? '';
const keycloakRoleMappings: Record<string, Record<string, UserRole> | undefined> = {
'realm-management': {
'realm-admin': new UserRole(
['iam_group', 'iam_role', 'iam_user', 'iam_realm'],
Permit.MANAGE
),
'manage-realm': new UserRole(
['iam_group', 'iam_role', 'iam_user', 'iam_realm'],
Permit.MANAGE
),
'view-realm': new UserRole(['iam_group', 'iam_role', 'iam_user', 'iam_realm'], Permit.READ),
'query-realm': new UserRole(['iam_group', 'iam_role', 'iam_user', 'iam_realm'], Permit.READ),
'view-users': new UserRole(['iam_user'], Permit.READ),
'query-users': new UserRole(['iam_user'], Permit.READ),
'manage-users': UserRole.combineRoles([new UserRole(['iam_user'], Permit.MANAGE)]),
'query-groups': new UserRole(['iam_group'], Permit.READ),
'view-groups': new UserRole(['iam_group'], Permit.READ),
},
account: {
'manage-account': new UserRole(
['account_user', 'account_application', 'account_group'],
Permit.MANAGE
),
'view-groups': new UserRole(['account_group'], Permit.READ),
},
};

export const parseKeycloakRoles = (stringRole: string) => {
const lookedURole = keycloakRoleMappings[stringRole] as UserRole | undefined;
if (scope === configuredClientId) {
return parseFHirRoles(stringRole);
}
const lookedURole = keycloakRoleMappings[scope]?.[stringRole] as UserRole | undefined;
return lookedURole;
};

Expand All @@ -72,28 +87,33 @@ export const adapter: RbacAdapter = (roles: KeycloakRoleData = defaultRoleData)
/**
parse each role, figure out which resource and verb permission it maps to and add that to the permission object
*/

let allRoleStrings = roles.realmAccess ?? [];
const invalidRoleStrings: string[] = [];
Object.values(roles.clientRoles ?? {}).forEach((roleArray) => {
allRoleStrings = [...allRoleStrings, ...roleArray];
});

const allRoleStrings = roles.realmAccess ?? [];
const allRoles: UserRole[] = [];

allRoleStrings.forEach((role) => {
// check if we can first get a hit from keycloak default roles.
let asRole = parseKeycloakRoles(role);

if (asRole === undefined) {
asRole = parseFHirRoles(role);
}
if (asRole) {
allRoles.push(asRole);
const invalidRoleStrings: string[] = [];
for (const role of allRoleStrings) {
const asRoleDef = parseFHirRoles(role);
if (asRoleDef) {
allRoles.push(asRoleDef);
} else {
invalidRoleStrings.push(role);
}
}
Object.entries(roles.clientRoles ?? {}).forEach(([scope, roleArray]) => {
roleArray.forEach((role) => {
// check if we can first get a hit from keycloak default roles.
let asRole = parseKeycloakClientRoles(scope, role);

if (asRole === undefined) {
asRole = parseFHirRoles(role);
}
if (asRole) {
allRoles.push(asRole);
} else {
invalidRoleStrings.push(role);
}
});
});

if (invalidRoleStrings.length > 0) {
/* eslint-disable no-console */
console.warn(`Could not understand the following roles: ${invalidRoleStrings.join(', ')}`);
Expand Down
31 changes: 16 additions & 15 deletions packages/rbac/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ export enum Permit {
MANAGE = 0b1111,
}

/**
* Authorization server resources that this web client is familiar with, thus we can enforce rbac for permissions on views that
* deal with the below resource.
*/
export const IamResources = ['iam_user', 'iam_role', 'iam_group'] as const;
export type IamResource = typeof IamResources[number];
/** Resources that the web client understands */

/** Resources that relate with user self administration like deleting own account */
export const accountClientResources = ['account_user', 'account_application', 'account_group'] as const

Check failure on line 14 in packages/rbac/src/constants.ts

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Replace `'account_user',·'account_application',·'account_group']·as·const` with `⏎··'account_user',⏎··'account_application',⏎··'account_group',⏎]·as·const;`
export type AccountClientResources = typeof accountClientResources[number]

Check failure on line 15 in packages/rbac/src/constants.ts

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Insert `;`

/** Resources that relate to the realm administration like managing other users accounts */
export const realmClientResources = ['iam_user', 'iam_realm', 'iam_group', 'iam_role'] as const

Check failure on line 18 in packages/rbac/src/constants.ts

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Insert `;`
export type RealmClientResources = typeof realmClientResources[number]

Check failure on line 19 in packages/rbac/src/constants.ts

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Insert `;`

/**
* fhir hapi Server resources that this web client is familiar with, thus we can enforce rbac for permissions on views that
* deal with the below resource.
*/
export const FhirResources = [
export const fhirResources = [
'Patient',
'Practitioner',
'PractitionerRole',
Expand All @@ -42,22 +45,20 @@ export const FhirResources = [
'Encounter',
'Flag',
] as const;
export type FhirResource = typeof FhirResources[number];
export type FhirResources = typeof fhirResources[number];

/**
* Roles for Situations where we have views that are not directly tied to any of the native fhir resources.
* These are custom and only relevant for the web. These should also be defined and parsed in a similar design as
* FhirResources
*/
export const WebCustomResources = ['WebDataImport'] as const;

export type WebCustomResource = typeof WebCustomResources[number];
export const webClientRoles = ['DataImport'] as const;
export type WebClientRoles = typeof webClientRoles[number];

export const KeycloakDefinedResources = [...FhirResources, ...WebCustomResources] as const;
export type KeycloakDefinedResource = typeof KeycloakDefinedResources[number];
export const allSupportedRoles = [...fhirResources, ...accountClientResources, ...realmClientResources, ...webClientRoles];

Check failure on line 58 in packages/rbac/src/constants.ts

View workflow job for this annotation

GitHub Actions / test (22.x, ubuntu-latest)

Replace `...fhirResources,·...accountClientResources,·...realmClientResources,·...webClientRoles` with `⏎··...fhirResources,⏎··...accountClientResources,⏎··...realmClientResources,⏎··...webClientRoles,⏎`
export type AllSupportedRoles = typeof allSupportedRoles[number];

export type AuthZResource = IamResource | FhirResource | WebCustomResource;
export type BinaryNumber = number;
export type PermitKey = keyof typeof Permit;
export type PermitKeyValues = Valueof<typeof Permit>;
export type ResourcePermitMap = Map<AuthZResource, number>;
export type ResourcePermitMap = Map<AllSupportedRoles, number>;
Loading

0 comments on commit b7c6d16

Please sign in to comment.