Skip to content

Commit

Permalink
Merge pull request #11 from agiledigital-labs/feature/IE-9
Browse files Browse the repository at this point in the history
IE-9 added support for deleting users
  • Loading branch information
dspasojevic authored Aug 29, 2022
2 parents b7825d1 + 8994e85 commit bdfb583
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 16 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# Oktagon CLI interface

This is a project to aid in the interaction with Okta.
CLI tools for managing users in Okta.

_Only use this tool in non-production environments, because it will bypass any MFA requirements of the organisation AND break the audit log that you would otherwise get in Okta._

### Node usage

After compilation using the `npm run build` command, run all commands using `./dist/oktagon <Command name>`.

### Command: list-users
### Common parameters

All `oktagon` commands require connecting to the Okta management APIs. The recommended way of doing this is to create an API-only application using the Admin console. The tool currently only supports private key authentication.

Provides a list of all users' logins, emails, display names, and statuses. Allows for environment variables under the name `OKTAGON_PK`, `OKTAGON_PRIVATE_KEY`, `OKTAGON_CID`, `OKTAGON_CLIENT_ID`. The environment variables correspond to the arguments `--pk`, `--private-key`, `--cid`, and `--client-id` respectively.
Allows for environment variables under the name `OKTAGON_PK`, `OKTAGON_PRIVATE_KEY`, `OKTAGON_CID`, `OKTAGON_CLIENT_ID`. The environment variables correspond to the arguments `--pk`, `--private-key`, `--cid`, and `--client-id` respectively.

The operation will only work if you have provided the correct client ID (in string form) and private key (in string form, must be JWT format e.g -pk "{JWK}").
33 changes: 26 additions & 7 deletions src/scripts/deactivate-user.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { UserStatus, User as OktaUser } from '@okta/okta-sdk-nodejs';
import { Argv } from 'yargs';
import { RootCommand } from '..';

import {
oktaManageClient,
OktaConfiguration,
user,
oktaUserAsUser,
User,
getUser,
} from './services/user-service';

const deactivateUser = async (
Expand All @@ -14,14 +16,31 @@ const deactivateUser = async (
): Promise<User> => {
const client = oktaManageClient(oktaConfiguration);

const oktaUser = await client.getUser(userId);
const maybeOktaUser = await getUser(userId, client);

// eslint-disable-next-line functional/no-expression-statement
await oktaUser.deactivate();
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
const deactivate = async (oktaUser: OktaUser) => {
// eslint-disable-next-line functional/no-expression-statement
await oktaUser.deactivate({
sendEmail: false,
});

const deactivatedOktaUser = await client.getUser(userId);

return oktaUserAsUser(deactivatedOktaUser);
};

const decativatedOktaUser = await client.getUser(userId);
// eslint-disable-next-line functional/functional-parameters
const throwOnMissing = () => {
// eslint-disable-next-line functional/no-throw-statement
throw new Error(`User [${userId}] does not exist. Can not de-activate.`);
};

return user(decativatedOktaUser);
return maybeOktaUser === undefined
? throwOnMissing()
: maybeOktaUser.status === UserStatus.DEPROVISIONED
? oktaUserAsUser(maybeOktaUser)
: deactivate(maybeOktaUser);
};

export default (
Expand Down Expand Up @@ -60,7 +79,7 @@ export default (
args.userId
);
// eslint-disable-next-line functional/no-expression-statement
console.info(user);
console.info(`De-activated [${user.id}] [${user.email}].`);
} catch (error: unknown) {
// eslint-disable-next-line functional/no-throw-statement
throw error instanceof Error
Expand Down
105 changes: 105 additions & 0 deletions src/scripts/delete-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { UserStatus, User as OktaUser } from '@okta/okta-sdk-nodejs';
import { Argv } from 'yargs';
import { RootCommand } from '..';

import {
oktaManageClient,
OktaConfiguration,
oktaUserAsUser,
User,
getUser,
} from './services/user-service';

const deleteUser = async (
oktaConfiguration: OktaConfiguration,
userId: string
): Promise<User> => {
const client = oktaManageClient(oktaConfiguration);

const maybeOktaUser = await getUser(userId, client);

// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
const performDelete = async (oktaUser: OktaUser) => {
// eslint-disable-next-line functional/no-expression-statement
await oktaUser.delete({
sendEmail: false,
});

return oktaUserAsUser(oktaUser);
};

// eslint-disable-next-line functional/functional-parameters
const throwOnNotDeprovisioned = () => {
// eslint-disable-next-line functional/no-throw-statement
throw new Error(
`User [${userId}] has not been deprovisioned. Deprovision before deleting.`
);
};

// eslint-disable-next-line functional/functional-parameters
const throwOnMissing = () => {
// eslint-disable-next-line functional/no-throw-statement
throw new Error(`User [${userId}] does not exist. Can not delete.`);
};

return maybeOktaUser === undefined
? throwOnMissing()
: maybeOktaUser.status === UserStatus.DEPROVISIONED
? performDelete(maybeOktaUser)
: throwOnNotDeprovisioned();
};

export default (
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
rootCommand: RootCommand
): Argv<{
readonly clientId: string;
readonly privateKey: string;
readonly organisationUrl: string;
readonly userId: string;
}> =>
rootCommand.command(
'delete-user [user-id]',
'Deletes the specified user',
// 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-id', {
describe: 'the identifier of the user to delete',
type: 'string',
demandOption: true,
});
},
async (args: {
readonly clientId: string;
readonly privateKey: string;
readonly organisationUrl: string;
readonly userId: string;
}) => {
// eslint-disable-next-line functional/no-try-statement
try {
const user = await deleteUser(
{
...args,
},
args.userId
);
// eslint-disable-next-line functional/no-expression-statement
console.info(`Deleted [${user.id}] [${user.email}].`);
} catch (error: unknown) {
// eslint-disable-next-line functional/no-throw-statement
throw error instanceof Error
? new Error(
`Failed to delete user [${args.userId}] in [${args.organisationUrl}].`,
{
cause: error,
}
)
: new Error(
`Failed to delete user [${args.userId}] in [${
args.organisationUrl
}] because of [${JSON.stringify(error)}].`
);
}
}
);
4 changes: 2 additions & 2 deletions src/scripts/list-users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { table } from 'table';
import {
oktaReadOnlyClient,
OktaConfiguration,
user,
oktaUserAsUser,
User,
} from './services/user-service';

Expand Down Expand Up @@ -34,7 +34,7 @@ const fetchUsers = async (
// eslint-disable-next-line functional/no-return-void, @typescript-eslint/prefer-readonly-parameter-types
.each((oktaUser) => {
// eslint-disable-next-line functional/immutable-data, functional/no-expression-statement
users.push(user(oktaUser));
users.push(oktaUserAsUser(oktaUser));
});

return users;
Expand Down
35 changes: 31 additions & 4 deletions src/scripts/services/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,24 @@ export type User = {
/**
* User status as a string.
*/
readonly status: string;
readonly status: okta.UserStatus;
};

/**
* Converts an Okta User into a simplified version that has only
* the information needed by the tool.
* @param oktaUser the Okta user to convert.
* @returns the converted User.
*/
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
export const user = (oktaUser: okta.User) => ({
export const oktaUserAsUser = (oktaUser: okta.User) => ({
id: oktaUser.id,
login: oktaUser.profile.login,
email: oktaUser.profile.email,
name: oktaUser.profile.displayName,
status: String(oktaUser.status),
name: [oktaUser.profile.firstName, oktaUser.profile.lastName]
.filter((s) => s.length > 0)
.join(' '),
status: oktaUser.status,
});

/**
Expand All @@ -46,6 +54,25 @@ export type OktaConfiguration = {
readonly organisationUrl: string;
};

/**
* Retrieves a user's details from Okta
* @param userId the id of the user whose details should be retrieved.
* @param client the client that should be used to retrieve the details.
* @returns either the user details or undefined if that user does not exist.
*/
export const getUser = async (
userId: string,
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
client: okta.Client
): Promise<okta.User | undefined> => {
return client.getUser(userId).catch((error) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return typeof error === 'object' && error.status === 404
? Promise.resolve(undefined)
: Promise.reject(error);
});
};

/**
* Creates a client that can read user information from Okta.
* @param oktaConfiguration configuration to use when construction the client.
Expand Down

0 comments on commit bdfb583

Please sign in to comment.