Skip to content

Commit

Permalink
Merge pull request #41 from agiledigital-labs/IE-345/organization-url…
Browse files Browse the repository at this point in the history
…-check

IE-345/organization-url-check + add url parser
  • Loading branch information
dspasojevic authored Nov 28, 2023
2 parents 96dd7f0 + fef29bd commit ca2ac64
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 33 deletions.
17 changes: 15 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import { readdirSync } from 'fs';
import { join } from 'path';
import yargs, { Argv } from 'yargs';
import { parseUrl } from './scripts/services/okta-service';
import * as E from 'fp-ts/lib/Either';

const organisationURL = 'organisation-url';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
Expand Down Expand Up @@ -52,14 +55,24 @@ const rootCommand = yargs
describe: 'Okta private key as string form of JSON',
demandOption: true,
})
.option('organisation-url', {
.option(organisationURL, {
type: 'string',
alias: ['org-url', 'ou'],
describe: 'Okta URL for Organisation',
demandOption: true,
})
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
.check(async (argv) => {
const result = await parseUrl(argv[organisationURL])();
// eslint-disable-next-line functional/no-conditional-statement
if (E.isLeft(result)) {
// eslint-disable-next-line functional/no-throw-statement
throw result.left;
}
return true;
})
.group(
['client-id', 'private-key', 'organisation-url'],
['client-id', 'private-key', organisationURL],
'Okta connection settings:'
)
.help();
Expand Down
20 changes: 17 additions & 3 deletions src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import { z } from 'zod';

export const oktaAPIError = z.object({
export const oktaAPIErrorSchema = z.object({
status: z.number(),
});

export type OktaAPIError = z.infer<typeof oktaAPIError>;
const urlStart = 'https://';
const urlEnd = '.okta.com';
export const urlSchema = z
.string()
.url()
.startsWith(urlStart, `URL must start with [${urlStart}].`)
//.co will cause fetch to time out
.endsWith(urlEnd, `URL must end with [${urlEnd}].`)
.min(
urlStart.length + urlEnd.length + 1,
'Domain name must be at least 1 character long.'
)
.refine(
(url) => !url.endsWith('-admin.okta.com'),
'Organisation URL should not be the admin URL. Please remove "-admin" and try again.'
);
40 changes: 25 additions & 15 deletions src/scripts/ping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,18 @@ describe('Pinging', () => {
organisationUrl: 'organisation url',
privateKey: '',
});
const clientId = '123';
const urlTemplate = 'https://template.okta.com';

it.each([200, 299])(
'should return a right when the okta server returns status [200-299] and credentials are correct',
'should return a right when the okta server returns status [200-299], and credentials along with organisation url are correct',
async (statusCode) => {
mockFetchRequest.mockResolvedValue({ status: statusCode });
mockOktaRequest.mockResolvedValue('resolved value');
const result = await validateOktaServerAndCredentials(
oktaClient,
'',
''
clientId,
urlTemplate
)();
expect(result).toEqualRight(true);
expect(mockFetchRequest).toHaveBeenCalledTimes(1);
Expand All @@ -66,8 +68,8 @@ describe('Pinging', () => {
mockFetchRequest.mockResolvedValue({ status: statusCode });
const result = await validateOktaServerAndCredentials(
oktaClient,
'',
''
clientId,
urlTemplate
)();
expect(result).toEqualLeft(
new Error('Server error. Please wait and try again later.')
Expand All @@ -87,12 +89,12 @@ describe('Pinging', () => {
mockFetchRequest.mockResolvedValue({ status: statusCode });
const result = await validateOktaServerAndCredentials(
oktaClient,
'',
''
clientId,
urlTemplate
)();
expect(result).toEqualLeft(
new Error(
'Client error. Please check your client id and the URL of your organisation.'
`Client error. Please check your client id [${clientId}] and the URL of your organisation [${urlTemplate}].`
)
);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
Expand All @@ -110,8 +112,8 @@ describe('Pinging', () => {
mockFetchRequest.mockResolvedValue({ status: statusCode });
const result = await validateOktaServerAndCredentials(
oktaClient,
'',
''
clientId,
urlTemplate
)();
expect(result).toEqualLeft(
new Error('Unexpected response from pinging okta server.')
Expand All @@ -127,7 +129,11 @@ describe('Pinging', () => {

it('should return a left when the ping request fails', async () => {
mockFetchRequest.mockRejectedValue('rejected value');
const result = await validateOktaServerAndCredentials(oktaClient, '', '')();
const result = await validateOktaServerAndCredentials(
oktaClient,
clientId,
urlTemplate
)();
expect(result).toEqualLeft(new Error('Failed to ping okta server.'));
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
expect((result as Left<Error>).left.cause).toEqual('rejected value');
Expand All @@ -152,11 +158,11 @@ describe('Pinging', () => {
mockOktaRequest.mockRejectedValue(error);
const result = await validateOktaServerAndCredentials(
oktaClient,
'',
''
clientId,
urlTemplate
)();
expect(result).toEqualLeft(
new Error('Failed to decode the private key.')
new Error('Client error. Please check your private key.')
);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
expect((result as Left<Error>).left.cause).toEqual(error);
Expand All @@ -168,7 +174,11 @@ describe('Pinging', () => {
it('should return a left when the okta server returns status [200-299] but credentials return an unknown error', async () => {
mockFetchRequest.mockResolvedValue({ status: 200 });
mockOktaRequest.mockRejectedValue('rejected value');
const result = await validateOktaServerAndCredentials(oktaClient, '', '')();
const result = await validateOktaServerAndCredentials(
oktaClient,
clientId,
urlTemplate
)();
expect(result).toEqualLeft(new Error('Failed to get access token.'));
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
expect((result as Left<Error>).left.cause).toEqual('rejected value');
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/ping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default (
rootCommand.command(
'ping',
// eslint-disable-next-line quotes
"Pings the okta server to see if it's running and check user credentials",
"Pings the okta server to see if it's running and check user credentials along with organisation url",
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
(yargs) => yargs.argv,
async (args: {
Expand Down
65 changes: 65 additions & 0 deletions src/scripts/services/okta-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* eslint-disable functional/no-return-void */
/* eslint-disable functional/no-expression-statement */

import { Left } from 'fp-ts/lib/Either';
import { parseUrl } from './okta-service';

/* eslint-disable functional/functional-parameters */
describe('Parsing url', () => {
it.each([
[
'',
[
{
code: 'invalid_string',
message: 'Invalid url',
path: [],
validation: 'url',
},
{
code: 'invalid_string',
message: 'URL must start with [https://].',
path: [],
validation: { startsWith: 'https://' },
},
{
code: 'invalid_string',
message: 'URL must end with [.okta.com].',
path: [],
validation: { endsWith: '.okta.com' },
},
{
code: 'too_small',
exact: false,
inclusive: true,
message: 'Domain name must be at least 1 character long.',
minimum: 18,
path: [],
type: 'string',
},
],
],
[
'https://trial-admin.okta.com',
[
{
code: 'custom',
message:
'Organisation URL should not be the admin URL. Please remove "-admin" and try again.',
path: [],
},
],
],
])(
'should return a left when the url is invalid',
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
async (url, issues) => {
const result = await parseUrl(url)();
expect(result).toEqualLeft(
new Error(`Client error. Invalid URL [${url}].`)
);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
expect((result as Left<Error>).left.cause).toEqual(issues);
}
);
});
34 changes: 22 additions & 12 deletions src/scripts/services/okta-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,24 @@ import * as okta from '@okta/okta-sdk-nodejs';
import * as TE from 'fp-ts/lib/TaskEither';
import { pipe } from 'fp-ts/lib/function';
import fetch from 'node-fetch';
import { oktaAPIError } from '../../schema';
import { oktaAPIErrorSchema, urlSchema } from '../../schema';

/**
* Parses a url to check whether it is valid
* @param url - the url to parse
* @returns a TaskEither that resolves to the url if it is valid, otherwise an error.
*/
export const parseUrl = (url: string): TE.TaskEither<Error, string> => {
const parsedURL = urlSchema.safeParse(url);
return parsedURL.success
? TE.right(parsedURL.data)
: TE.left(
new Error(`Client error. Invalid URL [${url}].`, {
cause: parsedURL.error.issues,
})
);
};

/**
* Validates the credentials provided to the tool.
* @param client - the Okta client to use to validate the credentials.
Expand All @@ -28,7 +43,7 @@ export const validateCredentials = (
TE.mapLeft((error) => {
const underlyingError =
error.cause instanceof Error ? error.cause : error;
const apiError = oktaAPIError.safeParse(underlyingError);
const apiError = oktaAPIErrorSchema.safeParse(underlyingError);
const underlyingErrorMessage = underlyingError.message;
// eslint-disable-next-line functional/no-conditional-statement
switch (true) {
Expand All @@ -46,7 +61,7 @@ export const validateCredentials = (
'error:0180006C:bignum routines::no inverse':
case underlyingErrorMessage ===
'error:1E08010C:DECODER routines::unsupported': {
return new Error('Failed to decode the private key.', {
return new Error('Client error. Please check your private key.', {
cause: underlyingError,
});
}
Expand Down Expand Up @@ -75,15 +90,10 @@ export const pingOktaServer = (
`${organisationUrl}/oauth2/default/.well-known/oauth-authorization-server?client_id=${clientId}`
);
},
(error: unknown) => {
// eslint-disable-next-line functional/no-conditional-statement
if (error instanceof Error) {
return error;
}
return new Error('Failed to ping okta server.', {
(error: unknown) =>
new Error('Failed to ping okta server.', {
cause: error,
});
}
})
),
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
TE.chain((response) => {
Expand All @@ -99,7 +109,7 @@ export const pingOktaServer = (
if (response.status >= 400 && response.status < 500) {
return TE.left(
new Error(
'Client error. Please check your client id and the URL of your organisation.',
`Client error. Please check your client id [${clientId}] and the URL of your organisation [${organisationUrl}].`,
{
cause: response,
}
Expand Down

0 comments on commit ca2ac64

Please sign in to comment.