diff --git a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html index 245a963fa..c5f9d3905 100644 --- a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html +++ b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html @@ -54,7 +54,7 @@ {{ 'IDP.SAML.BINDING' | translate }} - + {{ binding }} @@ -73,7 +73,7 @@
{{ 'IDP.SAML.NAMEIDFORMAT' | translate }} - + {{ nameIdFormat }} diff --git a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts index be2ce6e5a..cd1763311 100644 --- a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts +++ b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts @@ -7,7 +7,7 @@ import { SAMLBinding, SAMLNameIDFormat, } from '../../../proto/generated/zitadel/idp_pb'; -import { AbstractControl, FormGroup, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum'; import { ManagementService } from '../../../services/mgmt.service'; import { AdminService } from '../../../services/admin.service'; @@ -30,6 +30,18 @@ import { import { Environment, EnvironmentService } from '../../../services/environment.service'; import { filter, map } from 'rxjs/operators'; import { ProviderNextService } from '../provider-next/provider-next.service'; +import { getEnumKeys, getEnumKeyFromValue, convertEnumValuesToKeys } from '../../../utils/enum.utils'; + +interface SAMLProviderForm { + name: FormControl; + metadataXml: FormControl; + metadataUrl: FormControl; + binding: FormControl; + withSignedRequest: FormControl; + nameIdFormat: FormControl; + transientMappingAttributeName: FormControl; + federatedLogoutEnabled: FormControl; +} @Component({ selector: 'cnsl-provider-saml-sp', @@ -41,7 +53,7 @@ export class ProviderSamlSpComponent { public id: string | null = ''; public loading: boolean = false; public provider?: Provider.AsObject; - public form!: FormGroup; + public form!: FormGroup; public showOptional: boolean = false; public options: Options = new Options() .setIsCreationAllowed(true) @@ -51,8 +63,8 @@ export class ProviderSamlSpComponent { public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; // DEPRECATED: use service$ instead private service!: ManagementService | AdminService; - bindingValues: string[] = Object.keys(SAMLBinding); - nameIDFormatValues: string[] = Object.keys(SAMLNameIDFormat); + bindingValues: string[] = getEnumKeys(SAMLBinding); + nameIDFormatValues: string[] = getEnumKeys(SAMLNameIDFormat); public justCreated$: BehaviorSubject = new BehaviorSubject(''); public justActivated$ = new BehaviorSubject(false); @@ -118,16 +130,20 @@ export class ProviderSamlSpComponent { } private _initializeForm(): void { - this.form = new UntypedFormGroup( + const defaultBinding = getEnumKeyFromValue(SAMLBinding, SAMLBinding.SAML_BINDING_POST) || this.bindingValues[0]; + const defaultNameIdFormat = + getEnumKeyFromValue(SAMLNameIDFormat, SAMLNameIDFormat.SAML_NAME_ID_FORMAT_PERSISTENT) || this.nameIDFormatValues[0]; + + this.form = new FormGroup( { - name: new UntypedFormControl('', [requiredValidator]), - metadataXml: new UntypedFormControl('', []), - metadataUrl: new UntypedFormControl('', []), - binding: new UntypedFormControl(this.bindingValues[0], [requiredValidator]), - withSignedRequest: new UntypedFormControl(true, [requiredValidator]), - nameIdFormat: new UntypedFormControl(SAMLNameIDFormat.SAML_NAME_ID_FORMAT_PERSISTENT, []), - transientMappingAttributeName: new UntypedFormControl('', []), - federatedLogoutEnabled: new UntypedFormControl(false, []), + name: new FormControl('', { nonNullable: true, validators: [requiredValidator] }), + metadataXml: new FormControl('', { nonNullable: true }), + metadataUrl: new FormControl('', { nonNullable: true }), + binding: new FormControl(defaultBinding, { nonNullable: true, validators: [requiredValidator] }), + withSignedRequest: new FormControl(true, { nonNullable: true, validators: [requiredValidator] }), + nameIdFormat: new FormControl(defaultNameIdFormat, { nonNullable: true }), + transientMappingAttributeName: new FormControl('', { nonNullable: true }), + federatedLogoutEnabled: new FormControl(false, { nonNullable: true }), }, atLeastOneIsFilled('metadataXml', 'metadataUrl'), ); @@ -197,21 +213,19 @@ export class ProviderSamlSpComponent { : new AdminUpdateSAMLProviderRequest(); req.setId(this.provider?.id || this.justCreated$.value); - req.setName(this.name?.value); - if (this.metadataXml?.value) { + req.setName(this.name.value); + if (this.metadataXml.value) { req.setMetadataUrl(''); - req.setMetadataXml(this.metadataXml?.value); + req.setMetadataXml(this.metadataXml.value); } else { req.setMetadataXml(''); - req.setMetadataUrl(this.metadataUrl?.value); + req.setMetadataUrl(this.metadataUrl.value); } - req.setWithSignedRequest(this.withSignedRequest?.value); - // @ts-ignore - req.setBinding(SAMLBinding[this.binding?.value]); - // @ts-ignore - req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat?.value]); - req.setTransientMappingAttributeName(this.transientMapping?.value); - req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value); + req.setWithSignedRequest(this.withSignedRequest.value); + req.setBinding(SAMLBinding[this.binding.value as keyof typeof SAMLBinding]); + req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat.value as keyof typeof SAMLNameIDFormat]); + req.setTransientMappingAttributeName(this.transientMapping.value); + req.setFederatedLogoutEnabled(this.federatedLogoutEnabled.value); req.setProviderOptions(this.options); this.loading = true; @@ -235,24 +249,22 @@ export class ProviderSamlSpComponent { this.serviceType === PolicyComponentServiceType.MGMT ? new MgmtAddSAMLProviderRequest() : new AdminAddSAMLProviderRequest(); - req.setName(this.name?.value); - if (this.metadataXml?.value) { + req.setName(this.name.value); + if (this.metadataXml.value) { req.setMetadataUrl(''); - req.setMetadataXml(this.metadataXml?.value); + req.setMetadataXml(this.metadataXml.value); } else { req.setMetadataXml(''); - req.setMetadataUrl(this.metadataUrl?.value); + req.setMetadataUrl(this.metadataUrl.value); } req.setProviderOptions(this.options); - // @ts-ignore - req.setBinding(SAMLBinding[this.binding?.value]); - req.setWithSignedRequest(this.withSignedRequest?.value); + req.setBinding(SAMLBinding[this.binding.value as keyof typeof SAMLBinding]); + req.setWithSignedRequest(this.withSignedRequest.value); if (this.nameIDFormat) { - // @ts-ignore - req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat.value]); + req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat.value as keyof typeof SAMLNameIDFormat]); } - req.setTransientMappingAttributeName(this.transientMapping?.value); - req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value); + req.setTransientMappingAttributeName(this.transientMapping.value); + req.setFederatedLogoutEnabled(this.federatedLogoutEnabled.value); this.loading = true; this.service .addSAMLProvider(req) @@ -281,9 +293,20 @@ export class ProviderSamlSpComponent { .then((resp) => { this.provider = resp.idp; this.loading = false; - this.name?.setValue(this.provider?.name); + this.name.setValue(this.provider?.name || ''); if (this.provider?.config?.saml) { - this.form.patchValue(this.provider.config.saml); + const samlConfig = this.provider.config.saml; + const bindingKey = getEnumKeyFromValue(SAMLBinding, samlConfig.binding) || ''; + const nameIdFormatKey = getEnumKeyFromValue(SAMLNameIDFormat, samlConfig.nameIdFormat) || ''; + + this.form.patchValue({ + metadataXml: typeof samlConfig.metadataXml === 'string' ? samlConfig.metadataXml : '', + binding: bindingKey, + withSignedRequest: samlConfig.withSignedRequest, + nameIdFormat: nameIdFormatKey, + transientMappingAttributeName: samlConfig.transientMappingAttributeName || '', + federatedLogoutEnabled: samlConfig.federatedLogoutEnabled || false, + }); } }) .catch((error) => { @@ -296,50 +319,35 @@ export class ProviderSamlSpComponent { this._location.back(); } - compareBinding(value: string, index: number) { - if (value) { - return value === Object.keys(SAMLBinding)[index]; - } - return false; + private get name(): FormControl { + return this.form.controls.name; } - compareNameIDFormat(value: string, index: number) { - console.log(value, index); - if (value) { - return value === Object.keys(SAMLNameIDFormat)[index]; - } - return false; + private get metadataXml(): FormControl { + return this.form.controls.metadataXml; } - private get name(): AbstractControl | null { - return this.form.get('name'); + private get metadataUrl(): FormControl { + return this.form.controls.metadataUrl; } - private get metadataXml(): AbstractControl | null { - return this.form.get('metadataXml'); + private get binding(): FormControl { + return this.form.controls.binding; } - private get metadataUrl(): AbstractControl | null { - return this.form.get('metadataUrl'); + private get withSignedRequest(): FormControl { + return this.form.controls.withSignedRequest; } - private get binding(): AbstractControl | null { - return this.form.get('binding'); + private get nameIDFormat(): FormControl { + return this.form.controls.nameIdFormat; } - private get withSignedRequest(): AbstractControl | null { - return this.form.get('withSignedRequest'); + private get transientMapping(): FormControl { + return this.form.controls.transientMappingAttributeName; } - private get nameIDFormat(): AbstractControl | null { - return this.form.get('nameIdFormat'); - } - - private get transientMapping(): AbstractControl | null { - return this.form.get('transientMappingAttributeName'); - } - - private get federatedLogoutEnabled(): AbstractControl | null { - return this.form.get('federatedLogoutEnabled'); + private get federatedLogoutEnabled(): FormControl { + return this.form.controls.federatedLogoutEnabled; } } diff --git a/console/src/app/utils/enum.utils.ts b/console/src/app/utils/enum.utils.ts new file mode 100644 index 000000000..15b7d5076 --- /dev/null +++ b/console/src/app/utils/enum.utils.ts @@ -0,0 +1,84 @@ +/** + * Utility functions for working with TypeScript enums in Angular forms + * + * Example usage: + * ```typescript + * // Get dropdown options + * const bindingOptions = getEnumKeys(SAMLBinding); + * + * // Convert backend enum values to form-friendly string keys + * const formConfig = convertEnumValuesToKeys(backendConfig, { + * binding: SAMLBinding, + * nameIdFormat: SAMLNameIDFormat + * }); + * + * // Find specific enum key + * const defaultBinding = getEnumKeyFromValue(SAMLBinding, SAMLBinding.SAML_BINDING_POST); + * ``` + */ + +// Type constraint for TypeScript numeric enums +type NumericEnum = Record & Record; + +/** + * Get string keys from a TypeScript enum, excluding numeric reverse mappings + * @param enumObject The enum object + * @returns Array of string keys + * @example + * ```typescript + * const bindingOptions = getEnumKeys(SAMLBinding); + * // Returns: ['SAML_BINDING_UNSPECIFIED', 'SAML_BINDING_POST', ...] + * ``` + */ +export function getEnumKeys(enumObject: T): string[] { + return Object.keys(enumObject).filter((key) => isNaN(Number(key))); +} + +/** + * Find the string key for a given numeric enum value + * @param enumObject The enum object + * @param value The numeric enum value + * @returns The corresponding string key or undefined if not found + * @example + * ```typescript + * const key = getEnumKeyFromValue(SAMLBinding, 1); + * // Returns: 'SAML_BINDING_POST' + * ``` + */ +export function getEnumKeyFromValue(enumObject: T, value: number): string | undefined { + return Object.keys(enumObject).find((key) => enumObject[key] === value && isNaN(Number(key))); +} + +/** + * Convert enum values to string keys for form controls + * @param config Object containing enum values to convert + * @param enumMappings Object mapping config property names to enum objects + * @returns Modified config object with string keys + * @example + * ```typescript + * const formConfig = convertEnumValuesToKeys(samlConfig, { + * binding: SAMLBinding, + * nameIdFormat: SAMLNameIDFormat + * }); + * // Converts: { binding: 1, nameIdFormat: 2 } + * // To: { binding: 'SAML_BINDING_POST', nameIdFormat: 'SAML_NAME_ID_FORMAT_PERSISTENT' } + * ``` + */ +export function convertEnumValuesToKeys>( + config: T, + enumMappings: { [K in keyof Partial]: NumericEnum }, +): T { + const converted = { ...config }; + + for (const [propertyName, enumObject] of Object.entries(enumMappings)) { + const typedPropertyName = propertyName as keyof T; + if (converted[typedPropertyName] !== undefined && typeof converted[typedPropertyName] === 'number') { + const key = getEnumKeyFromValue(enumObject, converted[typedPropertyName] as number); + if (key) { + (converted as Record)[propertyName] = key; + } + } + } + + return converted; +}