Skip to content

Commit

Permalink
Merge branch 'refactor/oidc-rest-module' into 'develop'
Browse files Browse the repository at this point in the history
OEQ-2351 - refactor: OIDC rest module

See merge request edalex-group/development/oeq/openequella!498
  • Loading branch information
edalex-yinzi committed Dec 19, 2024
2 parents d22cfc0 + c1479ce commit 437543c
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 159 deletions.
55 changes: 30 additions & 25 deletions oeq-ts-rest-api/src/Oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export type IdentityProviderPlatform = 'AUTH0' | 'ENTRA_ID' | 'OKTA';
* Data structure for the common details of an Identity Provider, containing fields
* with primitive types.
**/
interface IdentityProviderBase {
interface CommonDetailsBase {
/**
* One of the supported Identity Provider: {@link IdentityProviderPlatform}
*/
Expand Down Expand Up @@ -78,7 +78,7 @@ interface IdentityProviderBase {
/**
* Full structure of an Identity Provider where types are looser in order to be handled by axios.
*/
interface IdentityProviderRaw extends IdentityProviderBase {
interface CommonDetailsRaw extends CommonDetailsBase {
/**
* A list of default OEQ roles to assign to the logged-in user.
*/
Expand All @@ -101,7 +101,7 @@ interface IdentityProviderRaw extends IdentityProviderBase {
/**
* Full structure of an Identity Provider, including default roles and custom role configuration.
*/
export interface IdentityProvider extends IdentityProviderBase {
export interface CommonDetails extends CommonDetailsBase {
/**
* A list of default OEQ roles to assign to the logged-in user.
*/
Expand All @@ -122,50 +122,53 @@ export interface IdentityProvider extends IdentityProviderBase {
}

/**
* Full structure for an Generic Identity Provider, including all the common details and specific
* fields related to interacting with the Identity Provider's API.
* API details configured for the use of an Identity Provider's REST APIs.
*/
export interface GenericIdentityProvider extends IdentityProvider {
platform: 'AUTH0' | 'ENTRA_ID';
export interface RestApiDetails {
/**
* The API endpoint for the Identity Provider, use for operations such as search for users
* The API endpoint for the Identity Provider, use for operations such as search for users.
*/
apiUrl: string;
/**
* Client ID used to get an Authorisation Token to use with the Identity Provider's API
* Client ID used to get an Authorisation Token to use with the Identity Provider's API.
*/
apiClientId: string;
/**
* Client Secret used with `apiClientId` to get an Authorization Token to use with the Identity Provider's API
* Client Secret used with `apiClientId` to get an Authorization Token to use with the Identity Provider's API.
*/
apiClientSecret?: string;
}

interface EntraId extends CommonDetails, RestApiDetails {
platform: 'ENTRA_ID';
}

interface Auth0 extends CommonDetails, RestApiDetails {
platform: 'AUTH0';
}

interface Okta extends CommonDetails, RestApiDetails {
platform: 'OKTA';
}

export type IdentityProvider = EntraId | Auth0 | Okta;

/**
* Full structure for the configuration of Okta, including all the common details for OIDC and API
* details for interacting with Core Okta API.
* Data structure for the response of an Identity Provider where common details are consolidated in one field and secret values are excluded.
*/
export interface Okta extends IdentityProvider {
platform: 'OKTA';
interface IdentityProviderResponse {
commonDetails: CommonDetailsRaw;
/**
* The base endpoint of Core Okta API, use for operations such as search for users
* The API endpoint for the Identity Provider, use for operations such as search for users.
*/
apiUrl: string;
/**
* Client ID used to request an access token to use with Core Okta API
* Client ID used to get an Authorisation Token to use with the Identity Provider's API.
*/
apiClientId: string;
}

/**
* Data structure for the response of an Identity Provider, which is slightly different
* as all the common fields are centralised into a single field 'commonDetails'.
*/
interface IdentityProviderResponse {
commonDetails: IdentityProviderRaw;
}

const toIdentityProviderRaw = (idp: IdentityProvider): IdentityProviderRaw => ({
const toIdentityProviderRaw = (idp: IdentityProvider): CommonDetailsRaw => ({
...idp,
defaultRoles: toStringArray(idp.defaultRoles),
roleConfig: pipe(
Expand Down Expand Up @@ -195,6 +198,8 @@ const toIdentityProvider = ({
O.toUndefined
),
...platformSpecific,
// Because server does not return the client secret, set it to undefined.
apiClientSecret: undefined,
});

/**
Expand Down
4 changes: 2 additions & 2 deletions oeq-ts-rest-api/test/Oidc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
import * as OEQ from '../src';
import { IdentityProviderCodec } from '../src/gen/Oidc';
import {
GenericIdentityProvider,
IdentityProvider,
getIdentityProvider,
updateIdentityProvider,
} from '../src/Oidc';
import * as TC from './TestConfig';

const auth0: GenericIdentityProvider = {
const auth0: IdentityProvider = {
platform: 'AUTH0',
issuer: 'https://dev-cqchwn4hfdb1p8xr.au.auth0.com',
authCodeClientId: 'C5tvBaB7svqjLPe0dDPBicgPcVPDJumZ',
Expand Down
130 changes: 48 additions & 82 deletions react-front-end/tsrc/settings/Integrations/oidc/OidcSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,12 @@ import {
import * as S from "fp-ts/string";
import SelectRoleControl from "../lti13/components/SelectRoleControl";
import {
defaultGeneralDetails,
defaultEntraIdApiDetails,
defaultApiDetailsMap,
defaultConfig,
generateGeneralDetails,
generateApiDetails,
ApiDetails,
generatePlatform,
oeqDetailsList,
defaultApiDetails,
} from "./OidcSettingsHelper";
import * as O from "fp-ts/Option";
import * as R from "fp-ts/Record";
Expand Down Expand Up @@ -150,11 +148,8 @@ const OidcSettings = ({
const [showValidationErrors, setShowValidationErrors] = useState(false);

// States for values displayed in different sections.
const [generalDetails, setGeneralDetails] =
useState<OEQ.Oidc.IdentityProvider>(defaultGeneralDetails);
const [apiDetails, setApiDetails] = useState<ApiDetails>(
defaultEntraIdApiDetails,
);
const [config, setConfig] =
useState<OEQ.Oidc.IdentityProvider>(defaultConfig);
const [defaultRoles, setDefaultRoles] = useState<ReadonlySet<RoleDetails>>(
new Set(),
);
Expand All @@ -168,29 +163,29 @@ const OidcSettings = ({
customRoles: undefined,
});

// TODO: remove this in OEQ-2359.
// Build final submit values for OIDC.
const currentOidcValue = {
...generalDetails,
...apiDetails,
const currentConfig = {
...config,
defaultRoles: pipe(roleIds(defaultRoles), RS.toSet),
roleConfig: generalDetails.roleConfig?.roleClaim
roleConfig: config.roleConfig?.roleClaim
? {
roleClaim: generalDetails.roleConfig.roleClaim,
roleClaim: config.roleConfig.roleClaim,
customRoles: transformCustomRoleMapping(customRoles),
}
: undefined,
};

// Initial configuration retrieved from server, but before the config is returned, use the defaults values of each state listed above.
const [initialIdpDetails, setInitialIdpDetails] =
useState<OEQ.Oidc.IdentityProvider>(currentOidcValue);
// Initial configuration retrieved from server, but before the config is returned, use the defaults values of "config".
const [initialConfig, setInitialConfig] =
useState<OEQ.Oidc.IdentityProvider>(currentConfig);

// Flag to indicate if there is an existing configuration in server.
const [serverHasConfiguration, setServerHasConfiguration] = useState(false);

const configurationChanged = hasConfigurationChanged(
initialIdpDetails,
currentOidcValue,
initialConfig,
currentConfig,
);

useEffect(() => {
Expand Down Expand Up @@ -253,24 +248,8 @@ const OidcSettings = ({
flow(
O.fold(constVoid, (idp) => {
setServerHasConfiguration(true);
setInitialIdpDetails(idp);
setGeneralDetails(idp);

if (OEQ.Codec.Oidc.GenericIdentityProviderCodec.is(idp)) {
setApiDetails({
platform: idp.platform,
apiUrl: idp.apiUrl,
apiClientId: idp.apiClientId,
apiClientSecret: idp.apiClientSecret,
});
} else if (OEQ.Codec.Oidc.OktaCodec.is(idp)) {
setApiDetails({
platform: idp.platform,
apiUrl: idp.apiUrl,
apiClientId: idp.apiClientId,
});
}

setInitialConfig(idp);
setConfig(idp);
// process role mappings value to display existing settings
setRoles(idp);
}),
Expand All @@ -279,46 +258,39 @@ const OidcSettings = ({
)();
}, [appErrorHandler, resolveRolesProvider]);

// Update the corresponding value in generalDetails state based on the provided key.
const onGeneralDetailsChange = (key: string, newValue: unknown) =>
setGeneralDetails({
...generalDetails,
// Update the corresponding value in idpConfigurations state based on the provided key.
const onConfigChange = (key: string, newValue: unknown) =>
setConfig({
...config,
[key]: newValue,
});

const onPlatformChange = (newValue: string) => {
// update platform
onGeneralDetailsChange("platform", newValue);
// Also clear the previous API configurations on platform change.
pipe(
defaultApiDetailsMap,
R.lookup(newValue),
O.map(setApiDetails),
O.getOrElseW(() => {
appErrorHandler(`Unsupported platform ${newValue}`);
}),
);
};

const onApiDetailsChange = (key: string, newValue: unknown) =>
setApiDetails({
...apiDetails,
[key]: newValue,
if (!OEQ.Codec.Oidc.IdentityProviderPlatformCodec.is(newValue)) {
throw new Error(`Unsupported platform ${newValue}`);
}

// Update platform and reset the API details.
setConfig({
...config,
platform: newValue,
...defaultApiDetails,
});
};

const generalDetailsRenderOptions = generateGeneralDetails(
generalDetails,
onGeneralDetailsChange,
config,
onConfigChange,
showValidationErrors,
serverHasConfiguration,
);

const apiDetailsRenderOptions = generateApiDetails(
apiDetails,
onApiDetailsChange,
config,
onConfigChange,
showValidationErrors,
// OKTA platform doesn't have client secret field which means there is no secret configuration in the server.
serverHasConfiguration && initialIdpDetails.platform !== "OKTA",
serverHasConfiguration && initialConfig.platform !== "OKTA",
);

const handleOnSave = async () => {
Expand All @@ -328,11 +300,11 @@ const OidcSettings = ({
pipe(
checkValidations(
generalDetailsRenderOptions,
R.fromEntries(Object.entries(generalDetails)),
R.fromEntries(Object.entries(config)),
) &&
checkValidations(
apiDetailsRenderOptions,
R.fromEntries(Object.entries(apiDetails)),
R.fromEntries(Object.entries(config)),
)
? TE.right(undefined)
: TE.left(checkForm),
Expand All @@ -341,21 +313,15 @@ const OidcSettings = ({
const validateStructure = (): TE.TaskEither<
string,
OEQ.Oidc.IdentityProvider
> => {
const codec =
currentOidcValue.platform === "OKTA"
? OEQ.Codec.Oidc.OktaCodec
: OEQ.Codec.Oidc.GenericIdentityProviderCodec;

return pipe(
currentOidcValue,
> =>
pipe(
currentConfig,
E.fromPredicate(
codec.is,
OEQ.Codec.Oidc.IdentityProviderCodec.is,
() => `Validation for the structure of OIDC configuration failed.`,
),
TE.fromEither,
);
};

const submit = (
oidcValue: OEQ.Oidc.IdentityProvider,
Expand All @@ -368,7 +334,7 @@ const OidcSettings = ({
TE.chain(submit),
TE.match(appErrorHandler, () => {
// Reset the initial values since user has saved the settings.
setInitialIdpDetails(currentOidcValue);
setInitialConfig(currentConfig);
setShowSnackBar(true);
}),
)();
Expand Down Expand Up @@ -400,7 +366,7 @@ const OidcSettings = ({
title={apiDetailsTitle}
desc={apiDetailsDesc}
fields={{
...generatePlatform(generalDetails.platform, onPlatformChange),
...generatePlatform(config.platform, onPlatformChange),
...apiDetailsRenderOptions,
}}
/>
Expand Down Expand Up @@ -431,23 +397,23 @@ const OidcSettings = ({
secondaryText={roleClaimDesc}
control={plainTextFiled({
name: roleClaimTitle,
value: generalDetails.roleConfig?.roleClaim,
value: config.roleConfig?.roleClaim,
disabled: false,
required: true,
onChange: (value) =>
setGeneralDetails({
...generalDetails,
setConfig({
...config,
roleConfig: {
roleClaim: value,
customRoles:
generalDetails.roleConfig?.customRoles ?? new Map(),
config.roleConfig?.customRoles ?? new Map(),
},
}),
showValidationErrors,
})}
/>

{generalDetails.roleConfig?.roleClaim && (
{config.roleConfig?.roleClaim && (
<>
<CustomRolesMappingControl
initialRoleMappings={customRoles}
Expand Down
Loading

0 comments on commit 437543c

Please sign in to comment.