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;
+}