Skip to content

Commit

Permalink
Add OktaUserGroupGateway without configuration
Browse files Browse the repository at this point in the history
Jira ticket: CAMS-283
  • Loading branch information
btposey committed Sep 16, 2024
1 parent c8b7c24 commit 6197900
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Collection, Group, User } from '@okta/okta-sdk-nodejs';
import { CamsUserGroup, CamsUserReference } from '../../../../../../common/src/cams/users';
import { OktaUserGroupGateway } from './okta-user-group-gateway';
import { createMockApplicationContext } from '../../../testing/testing-utilities';
import { UnknownError } from '../../../common-errors/unknown-error';

const listGroups = jest.fn();
const listGroupUsers = jest.fn();
jest.mock('@okta/okta-sdk-nodejs', () => {
return {
Client: function () {
return {
groupApi: {
listGroups,
listGroupUsers,
},
};
},
};
});

describe('OktaGroupGateway', () => {
describe('getUserGroups', () => {
const group1: Group = {
id: 'foo1',
profile: {
name: 'cams group name #1',
},
};

const group2: Group = {
id: 'foo2',
profile: {
name: 'cams group name #2',
},
};

const group3: Group = {
id: 'foo3',
profile: {
name: 'cams group name #3',
},
};

test('should return a list of CamsUserGroups', async () => {
listGroups.mockResolvedValue(buildMockCollection<Group>([group1, group2, group3]));

const actual = await OktaUserGroupGateway.getUserGroups(await createMockApplicationContext());

const expected: CamsUserGroup[] = [
{
id: group1.id,
name: group1.profile.name,
},
{
id: group2.id,
name: group2.profile.name,
},
{
id: group3.id,
name: group3.profile.name,
},
];
expect(actual).toEqual(expected);
});

test('should throw an error it an error is returned by the api', async () => {
listGroups.mockRejectedValue(new UnknownError('TEST-MODULE'));

await expect(
OktaUserGroupGateway.getUserGroups(await createMockApplicationContext()),
).rejects.toThrow();
});
});

describe('getUserGroupMembership', () => {
const camsUserGroup: CamsUserGroup = {
id: 'foo',
name: 'cams group name',
};

const user: User = {
id: '[email protected]',
profile: {
displayName: 'Abe Lincoln',
},
};

test('should return a list of CamsUsers', async () => {
listGroupUsers.mockResolvedValue(buildMockCollection<User>([user]));

const actual = await OktaUserGroupGateway.getUserGroupUsers(
await createMockApplicationContext(),
camsUserGroup,
);

const expected: CamsUserReference[] = [
{
id: user.id,
name: user.profile.displayName,
},
];
expect(actual).toEqual(expected);
});

test('should throw an error it an error is returned by the api', async () => {
listGroupUsers.mockRejectedValue(new UnknownError('TEST-MODULE'));

await expect(
OktaUserGroupGateway.getUserGroupUsers(await createMockApplicationContext(), camsUserGroup),
).rejects.toThrow();
});
});
});

function buildMockCollection<T>(dataToReturn: T[]): Collection<T> {
const currentItems = dataToReturn as unknown as Record<string, unknown>[];

let index = 0;
const collection: Collection<T> = {
nextUri: '',
httpApi: undefined,
factory: undefined,
currentItems,
request: undefined,
next: function (): Promise<{ done: boolean; value: T | null }> {
const nextResponse = { done: index === currentItems.length, value: null };
if (!nextResponse.done) {
nextResponse.value = currentItems[index];
index++;
}
return Promise.resolve(nextResponse);
},
getNextPage: function (): Promise<Record<string, unknown>[]> {
throw new Error('Function not implemented.');
},
each: function (
_iterator: (item: T) => Promise<unknown> | boolean | unknown,
): Promise<unknown> {
throw new Error('Function not implemented.');
},
subscribe: function (_config: {
interval?: number;
next: (item: T) => unknown | Promise<unknown>;
error: (e: Error) => unknown | Promise<unknown>;
complete: () => void;
}): { unsubscribe(): void } {
throw new Error('Function not implemented.');
},
[Symbol.asyncIterator]: function (): {
next: () => Promise<{ done: boolean; value: T | null }>;
} {
return { next: this.next };
},
};

return collection;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
Client,
GroupApiListGroupsRequest,
GroupApiListGroupUsersRequest,
} from '@okta/okta-sdk-nodejs';
import { CamsUserGroup, CamsUserReference } from '../../../../../../common/src/cams/users';
import { UserGroupGateway } from '../../types/authorization';
import { ApplicationContext } from '../../types/basic';
import { V2Configuration } from '@okta/okta-sdk-nodejs/src/types/configuration';
import { UnknownError } from '../../../common-errors/unknown-error';

const MODULE_NAME = 'OKTA_USER_GROUP_GATEWAY';
const MAX_PAGE_SIZE = 200;

let singleton: Client = undefined;

/**
* initialize
*
* Creates an Okta Client instance and retains it is module scope as a singleton.
* Subsequent calls to initialize return the previously created instance.
*
* See: https://github.com/okta/okta-sdk-nodejs?tab=readme-ov-file#oauth-20-authentication
* See: https://github.com/okta/okta-sdk-nodejs?tab=readme-ov-file#known-issues
*
* @param _context ApplicationContext
* @returns
*/
async function initialize(_context: ApplicationContext): Promise<Client> {
// EXAMPLE CODE
// const client = new okta.Client({
// orgUrl: 'https://dev-1234.oktapreview.com/',
// authorizationMode: 'PrivateKey',
// clientId: '{oauth application ID}',
// scopes: ['okta.users.manage'],
// privateKey: '{JWK}', // <-- see notes below
// keyId: 'kidValue'
// });
try {
const config: V2Configuration = {
// TODO: Map from context configuration
orgUrl: 'https://oktasubdomain/',
clientId: '{oauth application ID}',
authorizationMode: 'PrivateKey',
scopes: ['okta.groups.read'],
privateKey: '{ private key JSON }',
keyId: '',
};
if (!singleton) {
singleton = new Client(config);
}
return singleton;
} catch (originalError) {
throw new UnknownError(MODULE_NAME, { originalError });
}
}

/**
* getUserGroups
*
* Retrieves a list of Okta groups and transforms them into a list of CamsUserGroup.
*
* See: https://github.com/okta/okta-sdk-nodejs?tab=readme-ov-file#groups
* See: https://developer.okta.com/docs/api/
* See: https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Group/#tag/Group/operation/listGroups
*
* @param context ApplicationContext
* @returns CamsUserGroup[]
*/
async function getUserGroups(context: ApplicationContext): Promise<CamsUserGroup[]> {
const camsUserGroups: CamsUserGroup[] = [];
try {
const client = await initialize(context);
const query: GroupApiListGroupsRequest = {
limit: MAX_PAGE_SIZE,
};
const oktaGroups = await client.groupApi.listGroups(query);

for await (const oktaGroup of oktaGroups) {
camsUserGroups.push({
id: oktaGroup.id,
name: oktaGroup.profile.name,
});
}
} catch (originalError) {
throw new UnknownError(MODULE_NAME, { originalError });
}
return camsUserGroups;
}

/**
* getUserGroupUsers
*
* Retrieves a list of Okta users for a given Okta group and transforms them
* into a list of CamsUserReference.
*
* See: https://github.com/okta/okta-sdk-nodejs?tab=readme-ov-file#groups
* See: https://developer.okta.com/docs/api/
* See: https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Group/#tag/Group/operation/listGroupUsers
*
* @param context ApplicationContext
* @param group CamsUserGroup
* @returns CamsUserReference[]
*/
async function getUserGroupUsers(
context: ApplicationContext,
group: CamsUserGroup,
): Promise<CamsUserReference[]> {
const camsUserReferences: CamsUserReference[] = [];
try {
const client = await initialize(context);
const query: GroupApiListGroupUsersRequest = {
groupId: group.id,
limit: MAX_PAGE_SIZE,
};
const oktaUsers = await client.groupApi.listGroupUsers(query);

for await (const oktaUser of oktaUsers) {
camsUserReferences.push({
id: oktaUser.id,
name: oktaUser.profile.displayName,
});
}
} catch (originalError) {
throw new UnknownError(MODULE_NAME, { originalError });
}
return camsUserReferences;
}

export const OktaUserGroupGateway: UserGroupGateway = {
getUserGroups,
getUserGroupUsers,
};

export default OktaUserGroupGateway;
7 changes: 6 additions & 1 deletion backend/functions/lib/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { CasesCosmosDbRepository } from './adapters/gateways/cases.cosmosdb.repo
import ConsolidationOrdersCosmosDbRepository from './adapters/gateways/consolidations.cosmosdb.repository';
import { MockHumbleClient } from './testing/mock.cosmos-client-humble';
import { CosmosDbRepository } from './adapters/gateways/cosmos/cosmos.repository';
import { OpenIdConnectGateway } from './adapters/types/authorization';
import { OpenIdConnectGateway, UserGroupGateway } from './adapters/types/authorization';
import OktaGateway from './adapters/gateways/okta/okta-gateway';
import { UserSessionCacheRepository } from './adapters/gateways/user-session-cache.repository';
import { UserSessionCacheCosmosDbRepository } from './adapters/gateways/user-session-cache.cosmosdb.repository';
Expand All @@ -43,6 +43,7 @@ import LocalStorageGateway from './adapters/gateways/storage/local-storage-gatew
import MockAttorneysGateway from './testing/mock-gateways/mock-attorneys.gateway';
import { MockOrdersGateway } from './testing/mock-gateways/mock.orders.gateway';
import { MockOfficesGateway } from './testing/mock-gateways/mock.offices.gateway';
import OktaUserGroupGateway from './adapters/gateways/okta/okta-user-group-gateway';

export const getAttorneyGateway = (): AttorneyGatewayInterface => {
return MockAttorneysGateway;
Expand Down Expand Up @@ -167,3 +168,7 @@ export const getUserSessionCacheRepository = (
export const getStorageGateway = (_context: ApplicationContext): StorageGateway => {
return LocalStorageGateway;
};

export const getUserGroupGateway = (_context: ApplicationContext): UserGroupGateway => {
return OktaUserGroupGateway;
};

0 comments on commit 6197900

Please sign in to comment.