-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add OktaUserGroupGateway without configuration
Jira ticket: CAMS-283
- Loading branch information
Showing
3 changed files
with
299 additions
and
1 deletion.
There are no files selected for viewing
158 changes: 158 additions & 0 deletions
158
backend/functions/lib/adapters/gateways/okta/okta-user-group-gateway.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
135 changes: 135 additions & 0 deletions
135
backend/functions/lib/adapters/gateways/okta/okta-user-group-gateway.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters