/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0 */
import { AmazonProvider, GoogleProvider, IdentityProviderType, OIDCProvider, SAMLProvider } from '@ada/common';
import { COMMON_CLIENT_FIELD_DEFAULTS } from './provider-types/common';
import { CardSelectOption, CustomComponentTypes } from '$common/components';
import { DeepPartial } from '$ts-utils';
import { IdentityProviderDefinitions } from './provider-types';
import { IdentityProviderSummary } from '../../Summary';
import { LL } from '@ada/strings';
import { Link } from 'aws-northstar';
import { WizardStep } from '$northstar-plus';
import { cloneDeep, isNil, omitBy } from 'lodash';
import { componentTypes } from 'aws-northstar/components/FormRenderer/types';
import { identifierToName, nameToIdentifier } from '$common/utils';
import { validatorTypes } from 'aws-northstar/components/FormRenderer';
import React from 'react';
import type {
IdentityAttribute,
IdentityProvider,
IdentityProviderEntity,
IdentityProviderIdentifier,
IdentityProviderInput,
} from '@ada/api';
/* eslint-disable sonarjs/no-duplicate-string */
const PREFERRED_USERNAME_ATTRIBUTE = 'preferred_username';
export type FormData = Omit<
IdentityProviderInput,
'identityProviderId' | 'details' | 'attributeMapping' | 'enabled'
> & {
preferredUsername: string;
attributeMapping: { providerAttribute: string; cognitoAttribute: string }[];
[IdentityProviderType.Amazon]: { details: AmazonProvider };
[IdentityProviderType.Google]: { details: GoogleProvider };
[IdentityProviderType.OIDC]: { details: OIDCProvider };
[IdentityProviderType.SAML]: { details: SAMLProvider };
};
export const buildSteps = (
identityProviderId: string | undefined,
cognitoAttributes: IdentityAttribute[],
): WizardStep[] => {
const isNew = identityProviderId == null;
return [
{
title: isNew ? LL.ENTITY.IdentityProvider__CREATE() : LL.ENTITY.IdentityProvider__UPDATE(identityProviderId),
description: LL.VIEW.IDENTITY_PROVIDER.subtitle(),
fields: [
{
component: componentTypes.TEXT_FIELD,
name: 'name',
label: LL.ENTITY['IdentityProvider@'].name.label(),
description: LL.ENTITY['IdentityProvider@'].name.description(),
isRequired: true,
isReadOnly: !isNew,
validate: [
{
type: validatorTypes.REQUIRED,
},
{
type: validatorTypes.PATTERN,
pattern: '^[a-z][a-z0-9]+$',
},
],
},
{
component: componentTypes.TEXT_FIELD,
name: 'description',
label: LL.ENTITY['IdentityProvider@'].description.label(),
description: LL.ENTITY['IdentityProvider@'].description.description(),
placeholder: LL.ENTITY['IdentityProvider@'].description.placeholder(),
isRequired: true,
validate: [
{
type: validatorTypes.REQUIRED,
},
],
},
{
component: CustomComponentTypes.STRING_GROUP,
name: 'identifiers',
label: LL.ENTITY['IdentityProvider@'].identifiers.label(),
description: LL.ENTITY['IdentityProvider@'].identifiers.description(),
isOptional: true,
},
{
component: CustomComponentTypes.CARD_SELECT,
name: 'type',
label: LL.VIEW.IDENTITY_PROVIDER.WIZARD.FIELDS.type.label(),
description: LL.VIEW.IDENTITY_PROVIDER.WIZARD.FIELDS.type.description(),
isReadOnly: !isNew,
isRequired: true,
options: IdentityProviderDefinitions.map(
(idp): CardSelectOption => ({
title: idp.title,
subtitle: (
{LL.VIEW.IDENTITY_PROVIDER.WIZARD.FIELDS.type.learnMore()}
),
value: idp.type,
icon: idp.icon,
}),
),
validate: [
{
type: validatorTypes.REQUIRED,
},
],
},
...IdentityProviderDefinitions.flatMap((idp) => {
return {
component: componentTypes.SUB_FORM,
name: idp.type,
title: idp.title,
description: idp.description,
fields: idp.schemaFields,
condition: {
when: 'type',
is: idp.type,
},
};
}),
],
},
{
title: LL.ENTITY['IdentityProvider@'].attributeMapping.label(),
description: LL.ENTITY['IdentityProvider@'].attributeMapping.description(),
fields: [
{
component: componentTypes.TEXT_FIELD,
name: 'preferredUsername',
label: LL.ENTITY.IdentityProvider_.ATTRIBUTES.preferredUsername.label(),
description: LL.ENTITY.IdentityProvider_.ATTRIBUTES.preferredUsername.description(),
hintText: LL.ENTITY.IdentityProvider_.ATTRIBUTES.preferredUsername.hintText(),
isRequired: true,
validate: [
{
type: validatorTypes.REQUIRED,
},
],
},
{
component: componentTypes.FIELD_ARRAY,
name: 'attributeMapping',
label: LL.VIEW.IDENTITY_PROVIDER.WIZARD.FIELDS.attributeMapping.label(),
description: LL.VIEW.IDENTITY_PROVIDER.WIZARD.FIELDS.attributeMapping.description(),
helperText: LL.VIEW.IDENTITY_PROVIDER.WIZARD.FIELDS.attributeMapping.hintText(),
noItemsMessage: LL.VIEW.IDENTITY_PROVIDER.WIZARD.FIELDS.attributeMapping.emptyText(),
minItems: 0,
maxItems: 25,
fields: [
{
component: componentTypes.TEXT_FIELD,
name: 'providerAttribute',
label: LL.VIEW.IDENTITY_PROVIDER.WIZARD.FIELDS.attributeMapping.FIELDS.providerAttribute.label(),
isRequired: true,
validate: [
{
type: validatorTypes.REQUIRED,
},
],
},
{
component: componentTypes.SELECT,
name: 'cognitoAttribute',
label: LL.VIEW.IDENTITY_PROVIDER.WIZARD.FIELDS.attributeMapping.FIELDS.
cognitoAttribute.label(),
placeholder: LL.VIEW.IDENTITY_PROVIDER.WIZARD.FIELDS.attributeMapping.FIELDS.
cognitoAttribute.placeholder(),
isRequired: true,
options: cognitoAttributes
.filter((q) => !['sub', 'identities', PREFERRED_USERNAME_ATTRIBUTE].includes(q.name!))
.map(({ name }) => ({
label: name,
value: name,
})),
validate: [
{
type: validatorTypes.REQUIRED,
},
],
},
],
},
],
},
{
title: LL.VIEW.wizard.STEP.review.title(),
fields: [
{
component: componentTypes.REVIEW,
name: 'review',
Template: ({ data }: { data: FormData }) => {
const input = formDataToInput(data, true);
return ;
},
},
],
},
];
};
export const getInitialValues = (idp: IdentityProvider | undefined): DeepPartial => {
return entityToFormData(idp);
};
const DEFAULT_FORM_DATA: DeepPartial = {
// https://developer.amazon.com/docs/login-with-amazon/requesting-scopes-as-essential-voluntary.html
[IdentityProviderType.Amazon]: { details: { scopes: ['profile'] } },
[IdentityProviderType.Google]: { details: COMMON_CLIENT_FIELD_DEFAULTS },
[IdentityProviderType.OIDC]: { details: COMMON_CLIENT_FIELD_DEFAULTS },
[IdentityProviderType.SAML]: { details: {} },
};
export function entityToFormData(entity?: IdentityProviderEntity): DeepPartial {
const defaults = cloneDeep(DEFAULT_FORM_DATA);
if (entity == null) return defaults;
entity = cloneDeep(entity);
const attributeMapping: IdentityProviderEntity['attributeMapping'] = entity.attributeMapping || {};
const preferredUsername: string = attributeMapping[PREFERRED_USERNAME_ATTRIBUTE];
if (PREFERRED_USERNAME_ATTRIBUTE in attributeMapping) delete attributeMapping[PREFERRED_USERNAME_ATTRIBUTE];
const attributeMappingList: FormData['attributeMapping'] = Object.entries(attributeMapping).map(
([providerAttribute, cognitoAttribute]) => ({ providerAttribute, cognitoAttribute }),
);
const details: any = entity.details || {};
return {
...defaults,
name: identifierToName(entity.identityProviderId),
type: entity.type,
description: entity.description,
preferredUsername,
identifiers: entity.identifiers || [],
attributeMapping: attributeMappingList,
...(entity.type && entity.details ? { [entity.type]: { details } } : {}),
};
}
export function formDataToInput(
formData: FormData,
preview?: boolean,
): IdentityProviderInput & IdentityProviderIdentifier {
formData = cloneDeep(formData);
const details = omitBy(formData[formData.type].details, isNil);
delete formData[formData.type];
if (details.metadataFile?.content) {
if (preview) {
details.metadataFile = details.metadataFile.content;
} else {
details.metadataFile = btoa(details.metadataFile.content);
}
}
const attributeMapping = (formData.attributeMapping || []).reduce(
(mapping, attr) => {
return {
...mapping,
[attr.cognitoAttribute]: attr.providerAttribute,
};
},
{
[PREFERRED_USERNAME_ATTRIBUTE]: formData.preferredUsername,
} as IdentityProviderInput['attributeMapping'],
);
return {
identityProviderId: nameToIdentifier(formData.name),
name: formData.name,
description: formData.description,
type: formData.type,
identifiers: formData.identifiers,
attributeMapping,
details,
};
}