Skip to content

Commit

Permalink
Merge pull request #50 from agiledigital-labs/feature/IE-352-Okta-adm…
Browse files Browse the repository at this point in the history
…inistrator-can-list-a-users-groups-v2

IE-352 + oktagon: Add `--user` option to `list-groups` command
  • Loading branch information
dspasojevic authored Dec 14, 2023
2 parents 418bdd1 + 6870cc4 commit 58670c6
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 80 deletions.
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,21 @@ create-user okta.users.manage
deactivate-user okta.users.manage
delete-user okta.users.manage
list-groups okta.groups.read
list-groups [user] okta.users.read
add-user-to-group okta.groups.manage, okta.users.manage
remove-user-from-group okta.groups.manage, okta.users.manage
logs okta.logs.read
```

Attemting to run a command without the required permissions will most likely result in a 400 or 403 HTTPS error.
Attempting to run a command without the required permissions will most likely result in a 400 or 403 HTTPS error.

## Testing Guide

When testing the tool, a series of steps should always be performed before testing the new implementations.

To begin, after cloining the repository, install, run the linter, run the unit tests, and try to build the tool:
To begin, after cloning the repository, install, run the linter, run the unit tests, and try to build the tool:

```
```bash
npm i
npm run lint
npm run test
Expand All @@ -56,21 +57,28 @@ npm run build

Ensure that no errors or failed tests have been flagged in the process of running the code.

Next, try to attempt to connect to your designated okta sandbox or non-production server. Currently there is no tool to do this in isolation (See issue [IE-11](https://agiledigital.atlassian.net/browse/IE-11) on Jira), so it is reccomended to try and list all the users in the organisation:
Next, try to attempt to connect to your designated Okta sandbox or non-production server. Currently there is no tool to do this in isolation (See issue [IE-11](https://agiledigital.atlassian.net/browse/IE-11) on Jira), so it is recommended to try and list all the users in the organisation:

```
```bash
./dist/oktagon --ou <Okta Org. URL> --pk <Application Private Key> --cid <Application Client ID> list-users
```

Running this command should provide a list of non-deprovisioned users within the organisation. Invalid URLs and PKs will be flagged by the program. If you get a HTTPS page not found error, then check that your entered details are correct. If you get a HTTPS forbidden error, check that you have correctly granted the permission for your application to read users.

If trying to test commands relating to groups, try and run the list-groups and list-users [group] command as well:
If trying to test commands relating to groups, try and run the `list-groups` and `list-users` [group] command as well:

```
```bash
./dist/oktagon --ou <Okta Org. URL> --pk <Application Private Key> --cid <Application Client ID> list-groups
./dist/oktagon --ou <Okta Org. URL> --pk <Application Private Key> --cid <Application Client ID> list-users [group id]
```

Similarly, if trying to test commands relating to users, try and run the `list-users` and `list-groups [user]` command as well:

```bash
./dist/oktagon --ou <Okta Org. URL> --pk <Application Private Key> --cid <Application Client ID> list-users
./dist/oktagon --ou <Okta Org. URL> --pk <Application Private Key> --cid <Application Client ID> list-groups [user id]
```

Finally, always verify that the solution exists cleanly, and that the specific implementations are working in accordance with the suggested QA plan on the related Jira issue.

It is also reccomended that you try and store the three required arguments as environent variables while testing in order to reduce command line clutter. This is not required. See the section on Common/Global variables for information on how to do it.
It is also recommended that you try and store the three required arguments as environment variables while testing in order to reduce command line clutter. This is not required. See the section on Common/Global parameters for information on how to do it.
1 change: 1 addition & 0 deletions src/scripts/__fixtures__/data-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const baseUserService = (): UserService => ({
export const baseGroupService = (): GroupService => ({
getGroup: returnLeftTE,
listGroups: returnLeftTE,
listUserGroups: returnLeftTE,
addUserToGroup: returnLeftTE,
removeUserFromGroup: returnLeftTE,
});
143 changes: 90 additions & 53 deletions src/scripts/list-groups.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,119 @@
import { Argv } from 'yargs';
import { RootCommand } from '..';
import { type Argv } from 'yargs';
import { type RootCommand } from '..';

import { table } from 'table';
import {
OktaGroupService,
Group,
GroupService,
} from './services/group-service';
import { type Group, OktaGroupService } from './services/group-service';
import { oktaReadOnlyClient } from './services/client-service';

import * as TE from 'fp-ts/lib/TaskEither';
import * as E from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';
import * as Console from 'fp-ts/lib/Console';
import { ReadonlyURL } from 'readonly-types';
import * as TE from 'fp-ts/TaskEither';
import * as O from 'fp-ts/Option';
import { constant, pipe } from 'fp-ts/function';
import { type ReadonlyURL } from 'readonly-types';
import { handleTaskEither } from './services/error-service';

/**
* Tabulates group information for display.
* @param groups groups to be tabulated.
* @returns group information table formatted as a string.
*/
const groupsTable = (groups: readonly Group[]): string => {
return table(
const formatGroupsTable: (groups: readonly Group[]) => string = (
groups
): string =>
table(
[
['ID', 'Name', 'Type'],
...groups.map((group: Group) => [group.id, group.name, group.type]),
...groups.map(({ id, name, type }: Group) => [id, name, type]),
],
{
// eslint-disable-next-line functional/functional-parameters
drawHorizontalLine: () => false,
// eslint-disable-next-line functional/functional-parameters
drawVerticalLine: () => false,
drawHorizontalLine: constant(false),
drawVerticalLine: constant(false),
}
);
};

const groups = (service: GroupService) =>
/**
* User ID option.
*
* @example
* ```typescript
* const userIdOption: UserIdOption = O.some('00gddktac01w2dgmL5d7');
* ```
*/
export type UserIdOption = O.Option<string>;

/**
* Returns a `TaskEither` that resolves to a groups table string or rejects with
* an error, given an Okta group service and a user ID option. The groups table
* is generated from the list of all groups if the user ID option is `O.none`.
* Otherwise, the groups table consists of the list of groups that the user is a
* member of.
*
* @param userIdOption An Okta user ID option.
* @param oktaGroupService An Okta group service.
* @returns A `TaskEither` that resolves to a string or rejects with an error.
*/
const getGroupsListTableString: (
userIdOption: UserIdOption
) => (
oktaGroupService: Readonly<OktaGroupService>
) => TE.TaskEither<Error, string> = (userIdOption) => (oktaGroupService) =>
pipe(
service.listGroups(),
TE.map((groups) => groupsTable(groups)),
TE.chainFirstIOK(Console.info)
userIdOption,
O.match(oktaGroupService.listGroups, oktaGroupService.listUserGroups),
TE.map(formatGroupsTable)
);

export default (
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
rootCommand: RootCommand
): Argv<{
/**
* Options passed to the `list-groups` command. Includes the client ID, private
* key, organisation URL, and user ID (optional).
*
* @example
* ```typescript
* const listGroupsOptions: ListGroupsOptions = {
* clientId: '0oaddqhpa2nPVsxJX5d7',
* privateKey: <private key>,
* orgUrl: readonlyURL('https://dev-69870217.okta.com'),
* userId: '00uddtlbyrKj9nyAX5d7',
* };
* ```
*/
type ListGroupsOptions = {
readonly clientId: string;
readonly privateKey: string;
readonly orgUrl: ReadonlyURL;
}> =>
readonly userId?: string;
};

/**
* Builds the `list-groups` command.
*/
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
export default (rootCommand: RootCommand): Argv<ListGroupsOptions> =>
rootCommand.command(
'list-groups',
// eslint-disable-next-line quotes
"Provides a list of all groups' ID's, email addresses, display names, and statuses.",
// eslint-disable-next-line functional/no-return-void, functional/functional-parameters, @typescript-eslint/no-empty-function
() => {},
async (args: {
readonly clientId: string;
readonly privateKey: string;
readonly orgUrl: ReadonlyURL;
}) => {
const client = oktaReadOnlyClient(
{
clientId: args.clientId,
privateKey: args.privateKey,
orgUrl: args.orgUrl,
},
['groups']
"Provides a list of all groups' IDs, email addresses, display names, and statuses. Allows a specification of a user ID to list only groups that the user is a member of.",
// eslint-disable-next-line functional/no-return-void, @typescript-eslint/prefer-readonly-parameter-types
(yargs) => {
// eslint-disable-next-line functional/no-expression-statement
yargs.positional('user', {
type: 'string',
alias: ['user-id'],
// eslint-disable-next-line quotes
describe: "The user's ID",
});
},
// eslint-disable-next-line functional/no-return-void, @typescript-eslint/prefer-readonly-parameter-types
(listGroupsOptions) => {
const userIdOption = O.fromNullable(listGroupsOptions.userId);
return pipe(
new OktaGroupService(
oktaReadOnlyClient(
listGroupsOptions,
O.match(constant(['groups']), constant(['users']))(userIdOption)
)
),
getGroupsListTableString(userIdOption),
handleTaskEither
);
const service = new OktaGroupService(client);

const result = await groups(service)();

// eslint-disable-next-line functional/no-conditional-statement
if (E.isLeft(result)) {
// eslint-disable-next-line functional/no-throw-statement
throw result.left;
}
}
);
25 changes: 25 additions & 0 deletions src/scripts/services/error-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as TE from 'fp-ts/TaskEither';
import * as Console from 'fp-ts/Console';
import { flow } from 'fp-ts/function';

/**
* Handles a `TaskEither` instance by logging information to the console in case
* of success, or throwing an error if the computation fails.
*
* @template E The type of the error that the `TaskEither` might contain.
* @template A The type of the successful result that the `TaskEither` might
* contain.
* @param ma The `TaskEither` instance to handle.
* @returns {void}
* @throws {E} Throws the error contained in the `TaskEither` if it represents a
* failure.
*/
// eslint-disable-next-line functional/no-return-void
export const handleTaskEither: <E, A>(ma: TE.TaskEither<E, A>) => void = flow(
TE.tapIO(Console.info),
TE.getOrElse((error) => {
// eslint-disable-next-line functional/no-throw-statement, @typescript-eslint/no-throw-literal
throw error;
}),
(result) => void result()
);
75 changes: 56 additions & 19 deletions src/scripts/services/group-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as okta from '@okta/okta-sdk-nodejs';
import * as TE from 'fp-ts/lib/TaskEither';
import * as O from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/function';
import * as TE from 'fp-ts/TaskEither';
import * as O from 'fp-ts/Option';
import { constant, pipe } from 'fp-ts/function';

/**
* Subset of Group information provided by Okta. See okta.Group for further information on it's derived type.
Expand Down Expand Up @@ -127,43 +127,80 @@ export class OktaGroupService {
)
);

/**
* Returns a `TaskEither` that resolves to a list of all groups or rejects
* with an error.
*
* @returns A `TaskEither` that resolves to an array of groups or rejects with
* an error.
*/
// eslint-disable-next-line functional/functional-parameters
readonly listGroups = (): TE.TaskEither<Error, readonly Group[]> => {
// We need to populate groups with all of the client data so it can be
// returned. Okta's listGroups() function returns a custom collection that
// does not allow for any form of mapping, so array mutation is needed.

return TE.tryCatch(
readonly listGroups: () => TE.TaskEither<Error, readonly Group[]> = () =>
TE.tryCatch(
() => {
/* We need to populate groups with all of the client data so it can be
returned. */
// eslint-disable-next-line functional/prefer-readonly-type
const groups: Group[] = [];

return (
// eslint-disable-next-line functional/no-this-expression
this.client
.listGroups()
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
.each((oktaGroup) => {
// eslint-disable-next-line functional/immutable-data
return groups.push(oktaGroupAsGroup(oktaGroup));
})
// eslint-disable-next-line functional/functional-parameters
.then(() => {
return groups;
})
/* Okta's `listGroups` method returns a custom collection that does
not allow for any form of mapping, so array mutation is needed. */
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types, functional/immutable-data
.each((oktaGroup) => groups.push(oktaGroupAsGroup(oktaGroup)))
.then(constant(groups))
);
},
(error: unknown) =>
new Error('Failed to list groups.', {
cause: error,
})
);
};

/**
* Returns a `TaskEither` that resolves to a list of groups that the user with
* the given user ID is a member of or rejects with an error.
*
* @param userId The user ID of the user whose groups are to be listed.
* @returns A `TaskEither` that resolves to an array of groups or rejects with
* an error.
*/
readonly listUserGroups: (
userId: string
) => TE.TaskEither<Error, readonly Group[]> = (userId) =>
TE.tryCatch(
() => {
/* We need to populate groups with all of the client data so it can be
returned. */
// eslint-disable-next-line functional/prefer-readonly-type
const groups: Group[] = [];

return (
// eslint-disable-next-line functional/no-this-expression
this.client
.listUserGroups(userId)
/* Okta's `listUserGroups` method returns a custom collection that
does not allow for any form of mapping, so array mutation is needed.
*/
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types, functional/immutable-data
.each((oktaGroup) => groups.push(oktaGroupAsGroup(oktaGroup)))
.then(constant(groups))
);
},
(error: unknown) =>
new Error('Failed to list user groups.', {
cause: error,
})
);
}

export type GroupService = {
readonly getGroup: OktaGroupService['getGroup'];
readonly addUserToGroup: OktaGroupService['addUserToGroup'];
readonly removeUserFromGroup: OktaGroupService['removeUserFromGroup'];
readonly listGroups: OktaGroupService['listGroups'];
readonly listUserGroups: OktaGroupService['listUserGroups'];
};

0 comments on commit 58670c6

Please sign in to comment.