@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1 lines • 346 kB
Source Map (JSON)
{"version":3,"file":"c8y-ngx-components-auth-configuration.mjs","sources":["../../auth-configuration/basic-settings/basic-settings.model.ts","../../auth-configuration/basic-settings/basic-auth-settings.component.ts","../../auth-configuration/basic-settings/basic-auth-settings.component.html","../../auth-configuration/auth-configuration.guard.ts","../../auth-configuration/basic-settings/typed-option.ts","../../auth-configuration/basic-settings/tenant-login-option.mapper.ts","../../auth-configuration/basic-settings/auth-configuration.service.ts","../../auth-configuration/basic-settings/session-configuration.component.ts","../../auth-configuration/basic-settings/session-configuration.component.html","../../auth-configuration/basic-settings/login-settings.component.ts","../../auth-configuration/basic-settings/login-settings.component.html","../../auth-configuration/basic-settings/tfa-settings.component.ts","../../auth-configuration/basic-settings/tfa-settings.component.html","../../auth-configuration/basic-settings/auth-configuration.component.ts","../../auth-configuration/basic-settings/auth-configuration.component.html","../../auth-configuration/basic-settings/basic-settings.module.ts","../../auth-configuration/sso-configuration/sso-configuration.model.ts","../../auth-configuration/sso-configuration/sso-configuration.service.ts","../../auth-configuration/sso-configuration/template-parts/request-configuration.model.ts","../../auth-configuration/sso-configuration/template-parts/signature-configuration.model.ts","../../auth-configuration/sso-configuration/template-parts/external-token-config.model.ts","../../auth-configuration/sso-configuration/templates/custom-configuration-mapper.ts","../../auth-configuration/sso-configuration/templates/sso-configuration.mapper.ts","../../auth-configuration/sso-configuration/templates/template.component.ts","../../auth-configuration/sso-configuration/template-parts/basic-configuration.component.ts","../../auth-configuration/sso-configuration/template-parts/basic-configuration.component.html","../../auth-configuration/sso-configuration/template-parts/request-configuration.component.ts","../../auth-configuration/sso-configuration/template-parts/request-configuration.component.html","../../auth-configuration/sso-configuration/template-parts/user-id-configuration.component.ts","../../auth-configuration/sso-configuration/template-parts/user-id-configuration.component.html","../../auth-configuration/sso-configuration/template-parts/user-data-mapping.component.ts","../../auth-configuration/sso-configuration/template-parts/user-data-mapping.component.html","../../auth-configuration/sso-configuration/template-parts/access-mapping/dynamic-accesss-mapping-relations.model.ts","../../auth-configuration/sso-configuration/template-parts/access-mapping/child-predicates.component.ts","../../auth-configuration/sso-configuration/template-parts/access-mapping/child-predicates.component.html","../../auth-configuration/sso-configuration/template-parts/access-mapping/dynamic-access-mapping.component.ts","../../auth-configuration/sso-configuration/template-parts/access-mapping/dynamic-access-mapping.component.html","../../auth-configuration/sso-configuration/template-parts/access-mapping/inventory-roles-modal.component.ts","../../auth-configuration/sso-configuration/template-parts/access-mapping/inventory-roles-modal.component.html","../../auth-configuration/sso-configuration/template-parts/access-mapping/inventory-roles-mapping.component.ts","../../auth-configuration/sso-configuration/template-parts/access-mapping/inventory-roles-mapping.component.html","../../auth-configuration/sso-configuration/template-parts/access-mapping/access-mapping.component.ts","../../auth-configuration/sso-configuration/template-parts/access-mapping/access-mapping.component.html","../../auth-configuration/sso-configuration/template-parts/signature-configuration.component.ts","../../auth-configuration/sso-configuration/template-parts/signature-configuration.component.html","../../auth-configuration/sso-configuration/template-parts/external-token-config.component.ts","../../auth-configuration/sso-configuration/template-parts/external-token-config.component.html","../../auth-configuration/sso-configuration/templates/custom-template.component.ts","../../auth-configuration/sso-configuration/templates/custom-template.component.html","../../auth-configuration/sso-configuration/templates/key-cloak-configuration-mapper.ts","../../auth-configuration/sso-configuration/templates/key-cloak-template.component.ts","../../auth-configuration/sso-configuration/templates/key-cloak-template.component.html","../../auth-configuration/sso-configuration/templates/aad-configuration-mapper.ts","../../auth-configuration/sso-configuration/template-parts/logout-configuration.component.ts","../../auth-configuration/sso-configuration/template-parts/logout-configuration.component.html","../../auth-configuration/sso-configuration/templates/aad-template.component.ts","../../auth-configuration/sso-configuration/templates/aad-template.component.html","../../auth-configuration/sso-configuration/sso-configuration.component.ts","../../auth-configuration/sso-configuration/sso-configuration.component.html","../../auth-configuration/sso-configuration/sso-configuration.guard.ts","../../auth-configuration/sso-configuration/sso-configuration.module.ts","../../auth-configuration/factories/tabs.factory.ts","../../auth-configuration/factories/navigation.factory.ts","../../auth-configuration/pattern-messages.data.ts","../../auth-configuration/auth-configuration.module.ts","../../auth-configuration/c8y-ngx-components-auth-configuration.ts"],"sourcesContent":["import {\n IAuthenticationRestrictions,\n ITenantLoginOption,\n TenantLoginOptionType\n} from '@c8y/client';\n\nexport interface TenantLoginOption\n extends Omit<\n ITenantLoginOption,\n | 'authenticationRestrictions'\n | 'self'\n | 'strengthValidity'\n | 'tfaStrategy'\n | 'greenMinLength'\n | 'enforceStrength'\n | 'strengthValidity'\n | '_type'\n > {\n authenticationRestrictions?: AuthenticationRestrictions;\n}\n\nexport interface AuthenticationRestrictions\n extends Omit<IAuthenticationRestrictions, 'trustedUserAgents' | 'forbiddenUserAgents'> {\n trustedUserAgents: UserAgent[];\n forbiddenUserAgents: UserAgent[];\n}\n\nexport class UserAgent {\n readonly _id: string;\n value: string;\n\n constructor(value: string) {\n this._id = this.uniqueId();\n this.value = value;\n }\n\n get id() {\n return this._id;\n }\n\n private uniqueId() {\n const dateString = Date.now().toString(36);\n const randomString = Math.random().toString(36).substr(2);\n return dateString + randomString;\n }\n}\n\nexport function isOauthInternal(tenantLoginOption: TenantLoginOption) {\n return tenantLoginOption.type === TenantLoginOptionType.OAUTH2_INTERNAL;\n}\n\nexport function isBasic(tenantLoginOption: TenantLoginOption) {\n return tenantLoginOption.type === TenantLoginOptionType.BASIC;\n}\n","import { Component, Input, SimpleChanges } from '@angular/core';\nimport { TenantLoginOptionType } from '@c8y/client';\nimport { AuthConfiguration } from './auth-configuration.model';\nimport { ControlContainer, NgForm } from '@angular/forms';\nimport { UserAgent, AuthenticationRestrictions, isBasic } from './basic-settings.model';\n\n@Component({\n selector: 'c8y-basic-auth-settings',\n templateUrl: './basic-auth-settings.component.html',\n viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]\n})\nexport class BasicAuthSettingsComponent {\n @Input()\n authConfiguration: AuthConfiguration;\n\n preferredLoginOptionType: TenantLoginOptionType = TenantLoginOptionType.BASIC;\n\n tenantLoginOptionTypeEnum = TenantLoginOptionType;\n\n constructor(private controlContainer: ControlContainer) {}\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes.authConfiguration && changes.authConfiguration.currentValue) {\n this.preferredLoginOptionType =\n changes.authConfiguration.currentValue.preferredLoginOptionType;\n }\n }\n\n ngDoCheck() {\n if (this.preferredLoginOptionType !== this.authConfiguration.preferredLoginOptionType) {\n this.preferredLoginOptionType = this.authConfiguration.preferredLoginOptionType;\n if (this.preferredLoginOptionType === TenantLoginOptionType.BASIC) {\n this.forbiddenWebBrowsers = false;\n }\n }\n }\n\n get forbiddenWebBrowsers() {\n return this.authenticationRestrictions.forbiddenClients.includes('WEB_BROWSERS');\n }\n\n set forbiddenWebBrowsers(value: boolean) {\n this.authenticationRestrictions.forbiddenClients = value ? ['WEB_BROWSERS'] : [];\n }\n\n forbiddenUserAgentsRemove(id) {\n this.authenticationRestrictions.forbiddenUserAgents = this.remove(\n this.authenticationRestrictions.forbiddenUserAgents,\n id\n );\n this.controlContainer.control.markAsDirty();\n }\n\n trustedUserAgentsRemove(id) {\n this.authenticationRestrictions.trustedUserAgents = this.remove(\n this.authenticationRestrictions.trustedUserAgents,\n id\n );\n this.controlContainer.control.markAsDirty();\n }\n\n get authenticationRestrictions(): AuthenticationRestrictions {\n return this.authConfiguration.loginOptions.find(isBasic).authenticationRestrictions;\n }\n\n add(collection) {\n collection.push(new UserAgent(''));\n }\n\n private remove(collection, id) {\n const newArray = collection.filter(obj => obj.id !== id);\n if (!newArray.length) {\n newArray.push(new UserAgent(''));\n }\n return newArray;\n }\n}\n","<div\n class=\"card-block separator-top\"\n *ngIf=\"authConfiguration.preferredLoginOptionType !== 'BASIC'\"\n>\n <div class=\"col-sm-2\">\n <div class=\"h4 text-normal text-right text-left-xs\">\n {{ 'Basic Auth restrictions' | translate }}\n </div>\n </div>\n\n <div class=\"col-sm-9\">\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label class=\"c8y-switch\" title=\"{{ 'Forbidden for web browsers' | translate }}\">\n <input type=\"checkbox\" name=\"forbiddenWebBrowsers\" [(ngModel)]=\"forbiddenWebBrowsers\" />\n <span></span>\n <span>{{ 'Forbidden for web browsers' | translate }}</span>\n </label>\n <div\n class=\"alert alert-warning\"\n *ngIf=\"\n preferredLoginOptionType === tenantLoginOptionTypeEnum.BASIC && forbiddenWebBrowsers\n \"\n >\n {{\n 'You are about to forbid browsers from using Basic authentication. This will prevent users from using web applications on your tenant because you are going to set Basic authentication as the preferred login mode.'\n | translate\n }}\n </div>\n </c8y-form-group>\n </div>\n </div>\n\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <label title=\"{{ 'Forbidden user agents' | translate }}\">\n {{ 'Forbidden user agents' | translate }}\n </label>\n <div\n class=\"input-group m-t-8\"\n *ngFor=\"\n let forbiddenUserAgent of authenticationRestrictions.forbiddenUserAgents;\n last as isLast;\n first as isFirst\n \"\n >\n <input\n type=\"text\"\n [name]=\"'forbiddenUserAgent' + forbiddenUserAgent.id\"\n [(ngModel)]=\"forbiddenUserAgent.value\"\n class=\"form-control\"\n data-cy=\"c8y-basic-auth--forbidden-agent\"\n placeholder=\"{{ 'e.g.' | translate }} forbidden-agent\"\n />\n <div class=\"input-group-btn col-sm-2\">\n <button\n *ngIf=\"!(isFirst && isLast && forbiddenUserAgent.value === '')\"\n title=\"{{ 'Remove' | translate }}\"\n [name]=\"'forbiddenUserAgentRemove' + forbiddenUserAgent.id\"\n type=\"button\"\n class=\"btn btn-dot text-primary\"\n (click)=\"forbiddenUserAgentsRemove(forbiddenUserAgent.id)\"\n >\n <i class=\"dlt-c8y-icon-minus-circle text-danger\"></i>\n </button>\n <button\n title=\"{{ 'Add' | translate }}\"\n type=\"button\"\n class=\"btn btn-dot text-primary\"\n (click)=\"add(authenticationRestrictions.forbiddenUserAgents)\"\n *ngIf=\"isLast\"\n >\n <i class=\"dlt-c8y-icon-plus-circle\"></i>\n </button>\n </div>\n </div>\n </div>\n <div class=\"col-sm-6\">\n <label title=\"{{ 'Trusted user agents' | translate }}\">\n {{ 'Trusted user agents' | translate }}\n </label>\n <div\n class=\"input-group m-t-8\"\n *ngFor=\"\n let trustedUserAgent of authenticationRestrictions.trustedUserAgents;\n last as isLast;\n first as isFirst\n \"\n >\n <input\n type=\"text\"\n [name]=\"'trustedUserAgent' + trustedUserAgent.id\"\n class=\"form-control\"\n placeholder=\"{{ 'e.g.' | translate }} trusted-agent\"\n data-cy=\"c8y-basic-auth--trusted-agent\"\n [(ngModel)]=\"trustedUserAgent.value\"\n />\n <div class=\"input-group-btn col-sm-2\">\n <button\n *ngIf=\"!(isFirst && isLast && trustedUserAgent.value === '')\"\n title=\"{{ 'Remove' | translate }}\"\n [name]=\"'trustedUserAgentRemove' + trustedUserAgent.id\"\n type=\"button\"\n class=\"btn btn-dot btn-dot--danger text-primary\"\n (click)=\"trustedUserAgentsRemove(trustedUserAgent.id)\"\n >\n <i class=\"dlt-c8y-icon-minus-circle\"></i>\n </button>\n <button\n title=\"{{ 'Add' | translate }}\"\n type=\"button\"\n (click)=\"add(authenticationRestrictions.trustedUserAgents)\"\n class=\"btn btn-dot text-primary\"\n *ngIf=\"isLast\"\n >\n <i class=\"dlt-c8y-icon-plus-circle\"></i>\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n","import { Injectable } from '@angular/core';\n\nimport { Permissions } from '@c8y/ngx-components';\n\n@Injectable()\nexport class AuthConfigurationGuard {\n constructor(private permissions: Permissions) {}\n\n canActivate(): boolean {\n return this.permissions.hasAnyRole([\n Permissions.ROLE_TENANT_ADMIN,\n Permissions.ROLE_TENANT_MANAGEMENT_ADMIN\n ]);\n }\n}\n","import { ISystemOption, ITenantOption } from '@c8y/client';\nimport { isString } from 'lodash-es';\n\nexport class TypedOption implements ISystemOption, ITenantOption {\n constructor(\n public category: string,\n public key: string,\n public type: 'string' | 'boolean' | 'number',\n public defaultValue?: string | boolean | number,\n public value?: string\n ) {}\n\n apply(option: ISystemOption | ITenantOption) {\n this.category = option.category;\n this.key = option.key;\n this.value = option.value;\n }\n\n getValue() {\n try {\n return this.getValueByType();\n } catch (ex) {\n return this.defaultValue;\n }\n }\n\n private getValueByType() {\n switch (this.type) {\n case 'boolean':\n return this.value.toLowerCase() === 'true';\n case 'number':\n return this.parseNumberValue(this.value);\n case 'string':\n return this.parseStringValue(this.value);\n default:\n throw new TypeError();\n }\n }\n\n private parseNumberValue(stringValue: string) {\n const value = Number.parseInt(stringValue, 10);\n if (typeof value !== 'number' || isNaN(value)) {\n throw new Error();\n }\n return value;\n }\n\n private parseStringValue(value: string) {\n if (!isString(value)) {\n throw new Error();\n }\n return value;\n }\n}\n","import { Injectable } from '@angular/core';\nimport {\n UserAgent,\n AuthenticationRestrictions,\n TenantLoginOption,\n isBasic,\n isOauthInternal\n} from './basic-settings.model';\nimport { ITenantLoginOption, IAuthenticationRestrictions } from '@c8y/client';\nimport { defaults, isEmpty, omitBy, omit } from 'lodash-es';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class TenantLoginOptionMapper {\n mapTo(tenantLoginOption: ITenantLoginOption): TenantLoginOption {\n const loginOption: TenantLoginOption = omit(\n this.prapareTenantLoginOption(tenantLoginOption),\n 'authenticationRestrictions'\n );\n if (isBasic(loginOption)) {\n loginOption.authenticationRestrictions = this.mapAuthenticationRestrictionsTo(\n tenantLoginOption.authenticationRestrictions\n );\n }\n return loginOption;\n }\n\n mapFrom(\n originalLoginOption: TenantLoginOption,\n newLoginOption: TenantLoginOption\n ): ITenantLoginOption {\n if (isBasic(originalLoginOption)) {\n return this.mapBasicLoginOption(originalLoginOption, newLoginOption);\n }\n if (isOauthInternal(originalLoginOption)) {\n return this.mapOauthInternalLoginOption(originalLoginOption, newLoginOption);\n }\n throw new Error(\n `TenantLoginOptionMapper: The tenant login option cannot be mapped. Login option with type: ${originalLoginOption.type} is not supported.`\n );\n }\n\n private mapAuthenticationRestrictionsTo(\n authenticationRestrictions: IAuthenticationRestrictions\n ): AuthenticationRestrictions {\n const restrictions = defaults({}, omitBy(authenticationRestrictions, isEmpty), {\n forbiddenUserAgents: [''],\n trustedUserAgents: [''],\n forbiddenClients: []\n });\n restrictions.forbiddenUserAgents = restrictions.forbiddenUserAgents.map(\n val => new UserAgent(val)\n );\n restrictions.trustedUserAgents = restrictions.trustedUserAgents.map(val => new UserAgent(val));\n return restrictions;\n }\n\n private mapBasicLoginOption(\n originalLoginOption: TenantLoginOption,\n newLoginOption: TenantLoginOption\n ) {\n const loginOption = omit(originalLoginOption, ['sessionConfiguration']);\n loginOption.authenticationRestrictions = this.mapAuthenticationRestrictionsFrom(\n newLoginOption.authenticationRestrictions\n );\n return loginOption;\n }\n\n private mapOauthInternalLoginOption(\n originalLoginOption: TenantLoginOption,\n newLoginOption: TenantLoginOption\n ) {\n const loginOption = omit(originalLoginOption, ['authenticationRestrictions']);\n newLoginOption.sessionConfiguration !== null\n ? (loginOption.sessionConfiguration = newLoginOption.sessionConfiguration)\n : delete loginOption.sessionConfiguration;\n return loginOption;\n }\n\n private mapAuthenticationRestrictionsFrom(\n authenticationRestrictions: AuthenticationRestrictions\n ): IAuthenticationRestrictions {\n return {\n trustedUserAgents: authenticationRestrictions.trustedUserAgents\n .filter(({ value }) => value)\n .map(({ value }) => value),\n forbiddenUserAgents: authenticationRestrictions.forbiddenUserAgents\n .filter(({ value }) => value)\n .map(({ value }) => value),\n forbiddenClients: authenticationRestrictions.forbiddenClients.filter(value => value)\n };\n }\n\n private prapareTenantLoginOption(tenantLoginOption: ITenantLoginOption): TenantLoginOption {\n return omit(tenantLoginOption, [\n 'self',\n 'strengthValidity',\n 'tfaStrategy',\n 'greenMinLength',\n 'enforceStrength',\n 'strengthValidity',\n '_type'\n ]);\n }\n}\n","import { Injectable } from '@angular/core';\nimport {\n GrantType,\n IResult,\n ITenantLoginOption,\n ITenantOption,\n SystemOptionsService,\n TenantLoginOptionsService,\n TenantLoginOptionType,\n TenantOptionsService,\n TenantService,\n TfaStrategy,\n UserManagementSource\n} from '@c8y/client';\nimport { catchError, map } from 'rxjs/operators';\nimport { forkJoin, from, Observable, of } from 'rxjs';\nimport { defaults } from 'lodash-es';\nimport { AuthConfiguration, Options } from './auth-configuration.model';\nimport { AppStateService, TenantUiService } from '@c8y/ngx-components';\nimport { TypedOption } from './typed-option';\nimport { TenantLoginOption } from './basic-settings.model';\nimport { TenantLoginOptionMapper } from './tenant-login-option.mapper';\n\n@Injectable()\nexport class AuthConfigurationService {\n private systemOptionsWithDefaultValue: TypedOption[] = [\n new TypedOption('password', 'limit.validity', 'number', null),\n new TypedOption('password', 'enforce.strength', 'boolean', false),\n new TypedOption('two-factor-authentication', 'tenant-scope-settings.enabled', 'boolean', false),\n new TypedOption('two-factor-authentication', 'enabled', 'boolean', false),\n // note: this definition is inconsistent with backend and is overridden in getSystemOptions$\n new TypedOption('two-factor-authentication', 'enforced', 'boolean', false),\n new TypedOption('two-factor-authentication', 'enforced.group', 'string', '')\n ];\n\n private tenantOptionsWithDefaultValue: TypedOption[] = [\n new TypedOption('password', 'limit.validity', 'number', 0),\n new TypedOption('password', 'strength.validity', 'boolean', false),\n new TypedOption('two-factor-authentication', 'enabled', 'boolean', false),\n new TypedOption('two-factor-authentication', 'token.validity', 'number', 43200), // 30 days\n new TypedOption('two-factor-authentication', 'pin.validity', 'number', 30),\n new TypedOption('two-factor-authentication', 'enforced', 'boolean', false),\n new TypedOption('two-factor-authentication', 'strategy', 'string', 'SMS'),\n new TypedOption('oauth.internal', 'basic-token.lifespan.seconds', 'number', null),\n new TypedOption('configuration', 'tenant.login.ignore-case', 'boolean', false)\n ];\n\n constructor(\n private tenantLoginOptionsService: TenantLoginOptionsService,\n private tenantOptionsService: TenantOptionsService,\n private systemOptionsService: SystemOptionsService,\n private appState: AppStateService,\n private tenantUiService: TenantUiService,\n private tenantLoginOptionMapper: TenantLoginOptionMapper,\n private tenantService: TenantService\n ) {}\n\n getAuthConfiguration$(): Observable<AuthConfiguration> {\n const loginOptions$ = this.getLoginOptions$();\n return forkJoin({\n loginOptions: this.map(loginOptions$),\n tenantOptions: this.getTenantOptions$(),\n systemOptions: this.getSystemOptions$(),\n smsGatewayAvailable: this.isSmsApplicationAvailable$(),\n preferredLoginOptionType: this.getPreferredLoginOptionType$(loginOptions$)\n });\n }\n\n save(newAuthConfiguration: AuthConfiguration, previousAuthConfiguration: AuthConfiguration) {\n const tenantOptions = this.prepareTenantOptions(\n newAuthConfiguration,\n previousAuthConfiguration\n );\n const updateTenantOptions = tenantOptions.map(tenantOption => {\n const fixedOption = this.fixTfaStrategy(tenantOption);\n if (fixedOption) {\n return fixedOption;\n }\n return this.tenantOptionsService.create(tenantOption);\n });\n const basicLoginOption = this.prepareBasicLoginOption(\n newAuthConfiguration,\n previousAuthConfiguration\n );\n const oauthInternalLoginOption = this.prepareOauthInternalLoginOption(\n newAuthConfiguration,\n previousAuthConfiguration\n );\n\n return Promise.all([\n this.saveOrUpdateLoginOption(basicLoginOption),\n this.saveOrUpdateLoginOption(oauthInternalLoginOption),\n ...updateTenantOptions\n ]);\n }\n\n private map(loginOptions$: Observable<ITenantLoginOption[]>): Observable<TenantLoginOption[]> {\n return loginOptions$.pipe(\n map(loginOptions =>\n loginOptions.map(loginOption => this.tenantLoginOptionMapper.mapTo(loginOption))\n )\n );\n }\n\n private saveOrUpdateLoginOption(\n loginOption: ITenantLoginOption\n ): Promise<IResult<ITenantLoginOption>> {\n return loginOption.id\n ? this.tenantLoginOptionsService.update(loginOption)\n : this.tenantLoginOptionsService.create(loginOption);\n }\n\n private prepareBasicLoginOption(\n newAuthConfiguration: AuthConfiguration,\n previousAuthConfiguration: AuthConfiguration\n ): ITenantLoginOption {\n const basicLoginOption = this.originalLoginOptionWithDefaults(\n previousAuthConfiguration,\n TenantLoginOptionType.BASIC\n );\n basicLoginOption.visibleOnLoginPage = this.visibleOnLoginPage(\n newAuthConfiguration,\n TenantLoginOptionType.BASIC\n );\n return this.tenantLoginOptionMapper.mapFrom(\n basicLoginOption,\n this.getLoginOptionFromAuthConfiguration(newAuthConfiguration, TenantLoginOptionType.BASIC)\n );\n }\n\n private prepareOauthInternalLoginOption(\n newAuthConfiguration: AuthConfiguration,\n previousAuthConfiguration: AuthConfiguration\n ): ITenantLoginOption {\n const oauthInternalLoginOption = this.originalLoginOptionWithDefaults(\n previousAuthConfiguration,\n TenantLoginOptionType.OAUTH2_INTERNAL\n );\n oauthInternalLoginOption.visibleOnLoginPage = this.visibleOnLoginPage(\n newAuthConfiguration,\n TenantLoginOptionType.OAUTH2_INTERNAL\n );\n return this.tenantLoginOptionMapper.mapFrom(\n oauthInternalLoginOption,\n this.getLoginOptionFromAuthConfiguration(\n newAuthConfiguration,\n TenantLoginOptionType.OAUTH2_INTERNAL\n )\n );\n }\n\n private originalLoginOptionWithDefaults(\n previousAuthConfiguration: AuthConfiguration,\n loginOptionType: TenantLoginOptionType\n ): TenantLoginOption {\n return defaults(\n {},\n this.getLoginOptionFromAuthConfiguration(previousAuthConfiguration, loginOptionType),\n this.getDefaultLoginOption(loginOptionType)\n );\n }\n\n private getLoginOptionFromAuthConfiguration(\n authConfiguration: AuthConfiguration,\n loginOptionType: TenantLoginOptionType\n ) {\n return authConfiguration.loginOptions.find(loginOption => loginOption.type === loginOptionType);\n }\n\n private visibleOnLoginPage(\n authConfiguration: AuthConfiguration,\n loginOptionType: TenantLoginOptionType\n ): boolean {\n return authConfiguration.preferredLoginOptionType === loginOptionType;\n }\n\n private prepareTenantOptions(\n newAuthConfiguration: AuthConfiguration,\n previousAuthConfiguration: AuthConfiguration\n ): ITenantOption[] {\n const getValue = (authCfg, tenantOption) =>\n authCfg.tenantOptions[tenantOption.category][tenantOption.key];\n const hasChanged = tenantOption =>\n getValue(newAuthConfiguration, tenantOption) !==\n getValue(previousAuthConfiguration, tenantOption);\n\n return this.tenantOptionsWithDefaultValue\n .filter(tenantOption => getValue(newAuthConfiguration, tenantOption) !== null)\n .filter(tenantOption => hasChanged(tenantOption))\n .map(tenantOption => ({\n category: tenantOption.category,\n key: tenantOption.key,\n value: getValue(newAuthConfiguration, tenantOption).toString()\n }));\n }\n\n private getLoginOptions$(): Observable<ITenantLoginOption[]> {\n return forkJoin([\n this.getLoginOption(TenantLoginOptionType.OAUTH2),\n this.getLoginOption(TenantLoginOptionType.BASIC),\n this.getLoginOption(TenantLoginOptionType.OAUTH2_INTERNAL)\n ]).pipe(map(loginOptions => loginOptions.filter(loginOption => !!loginOption)));\n }\n\n private getLoginOption(\n tenantLoginOptionType: TenantLoginOptionType\n ): Observable<ITenantLoginOption> {\n return from(this.tenantLoginOptionsService.detail(tenantLoginOptionType)).pipe(\n map(res => res.data),\n catchError(() =>\n tenantLoginOptionType !== TenantLoginOptionType.OAUTH2\n ? of(this.getDefaultLoginOption(tenantLoginOptionType))\n : of(null)\n )\n );\n }\n\n private getPreferredLoginOptionType$(\n loginOptions$: Observable<ITenantLoginOption[]>\n ): Observable<TenantLoginOptionType> {\n return loginOptions$.pipe(\n map(loginOptions => {\n return this.tenantUiService.getPreferredLoginOption(loginOptions).type;\n })\n );\n }\n\n private getTenantOptions$(): Observable<Options> {\n return forkJoin(\n this.tenantOptionsWithDefaultValue.map((option: TypedOption) =>\n from(this.tenantOptionsService.detail(option)).pipe(\n map(res => {\n option.apply(res.data);\n return option;\n }),\n catchError(() => of(option))\n )\n )\n ).pipe(map(options => this.getOptionsObject(options)));\n }\n\n private getSystemOptions$(): Observable<Options> {\n return forkJoin(\n this.systemOptionsWithDefaultValue.map((option: TypedOption) => {\n const fixedOption = this.fixTfaEnforcedSystemOption(option);\n if (fixedOption) {\n return fixedOption;\n }\n\n return from(this.systemOptionsService.detail(option)).pipe(\n map(res => {\n option.apply(res.data);\n return option;\n }),\n catchError(() => of(option))\n );\n })\n ).pipe(map(options => this.getOptionsObject(options)));\n }\n\n /**\n * Returns an observable with fixed `two-factor-authentication.enforced` system option or null.\n * This method fixes problem with inconsistent value. System option `two-factor-authentication.enforced` is list of tenants when UI using boolean value.\n * This part will be removed after implementing new endpoint in MTM-50490.\n */\n private fixTfaEnforcedSystemOption(option: TypedOption): Observable<TypedOption> {\n if (option.category === 'two-factor-authentication' && option.key === 'enforced') {\n return from(this.tenantService.getTfaSettings(this.tenantUiService.currentTenant)).pipe(\n map(tfaSettings => {\n option.value = tfaSettings.enforcedOnSystemLevel.toString();\n return option;\n })\n );\n }\n return null;\n }\n\n /**\n * Returns a promise or null.\n * This method is needed now, because simply changing TFA strategy tenant option does not trigger all the necessary backend logic to apply the change, therefore, we need to call tenant's `/tfa` endpoint, which applies the change for all users in the tenant.\n * Within MTM-50490, we're going to simplify the process further by replacing multiple requests with one new endpoint that will handle saving of all authentication settings.\n */\n private fixTfaStrategy(option: ITenantOption): Promise<IResult<null>> {\n if (option.category === 'two-factor-authentication' && option.key === 'strategy') {\n const strategy: TfaStrategy = option.value === 'SMS' ? TfaStrategy.SMS : TfaStrategy.TOTP;\n return this.tenantService.updateTfaStrategy(this.tenantUiService.currentTenant, strategy);\n }\n return null;\n }\n\n private isSmsApplicationAvailable$(): Observable<boolean> {\n return from(this.appState.isApplicationAvailable('sms-gateway'));\n }\n\n private getOptionsObject(options: TypedOption[]) {\n return options.reduce((optionsObject, option) => {\n optionsObject[option.category] = optionsObject[option.category] || {};\n optionsObject[option.category][option.key] = option.getValue();\n return optionsObject;\n }, {});\n }\n\n private getDefaultLoginOption(tenantLoginOptionType: TenantLoginOptionType): ITenantLoginOption {\n return {\n userManagementSource: UserManagementSource.INTERNAL,\n grantType: GrantType.PASSWORD,\n providerName: 'Cumulocity',\n visibleOnLoginPage: false,\n type: tenantLoginOptionType\n };\n }\n}\n","import { Component, Input, SimpleChanges } from '@angular/core';\nimport { ISessionConfiguration, TenantLoginOptionType } from '@c8y/client';\nimport { ControlContainer, NgForm } from '@angular/forms';\nimport { defaults, cloneDeep, isFinite } from 'lodash-es';\nimport { gettext, TenantUiService } from '@c8y/ngx-components';\nimport { AuthConfiguration } from './auth-configuration.model';\nimport { TranslateService } from '@ngx-translate/core';\nimport { isOauthInternal } from './basic-settings.model';\n\n@Component({\n selector: 'c8y-session-configuration',\n templateUrl: './session-configuration.component.html',\n viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]\n})\nexport class SessionConfigurationComponent {\n @Input()\n authConfiguration: AuthConfiguration;\n tenantLoginOptionTypeEnum = TenantLoginOptionType;\n ABSOLUTE_TIMEOUT_VALIDATION_MESSAGE = gettext(\n 'The value must be greater than \"Token lifespan\" and not less than {{ minAbsoluteTimeout }}.'\n );\n RENEWAL_TIMEOUT_VALIDATION_MESSAGE = gettext('The value must be less than \"Token lifespan\".');\n MAX_TOKEN_LIFESPAN_VALIDATION_MESSAGE = gettext(\n 'The value must be less than \"Session absolute timeout\".'\n );\n MIN_TOKEN_LIFESPAN_VALIDATION_MESSAGE = gettext(\n 'The value must be greater than \"Session renewal timeout\".'\n );\n\n USER_AGENT_VALIDATION_REQUIRED_POPOVER = gettext(\n 'If selected, then every request needs to use the same \"User-Agent\" header as the first request which initiated the session.'\n );\n\n private MIN_ABSOLUTE_TIMEOUT: number = 15 * 60;\n private originalSessionConfiguration;\n private previousTokenLifespan;\n\n constructor(\n private tenantUiService: TenantUiService,\n private translateService: TranslateService\n ) {\n this.ABSOLUTE_TIMEOUT_VALIDATION_MESSAGE = this.translateService.instant(\n this.ABSOLUTE_TIMEOUT_VALIDATION_MESSAGE,\n { minAbsoluteTimeout: this.MIN_ABSOLUTE_TIMEOUT }\n );\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes.authConfiguration && changes.authConfiguration.currentValue) {\n const oauthInternal =\n changes.authConfiguration.currentValue.loginOptions.find(isOauthInternal) || {};\n this.originalSessionConfiguration = cloneDeep(oauthInternal.sessionConfiguration);\n this.sessionConfiguration = oauthInternal.sessionConfiguration;\n this.previousTokenLifespan =\n this.authConfiguration.tenantOptions['oauth.internal']['basic-token.lifespan.seconds'];\n }\n }\n\n get renewalTimeoutSeconds(): number {\n const sessionConfiguration = this.sessionConfiguration;\n return this.convertToSeconds(sessionConfiguration.renewalTimeoutMillis);\n }\n\n set renewalTimeoutSeconds(value: number) {\n this.sessionConfiguration.renewalTimeoutMillis = this.convertToMillis(value);\n }\n\n get absoluteTimeoutSeconds(): number {\n const sessionConfiguration = this.sessionConfiguration;\n return this.convertToSeconds(sessionConfiguration.absoluteTimeoutMillis);\n }\n\n set absoluteTimeoutSeconds(value: number) {\n this.sessionConfiguration.absoluteTimeoutMillis = this.convertToMillis(value);\n }\n\n get maximumNumberOfParallelSessions(): number {\n return this.sessionConfiguration.maximumNumberOfParallelSessions;\n }\n\n set maximumNumberOfParallelSessions(value: number) {\n this.sessionConfiguration.maximumNumberOfParallelSessions = value;\n }\n\n get userAgentValidationRequired(): boolean {\n return this.sessionConfiguration.userAgentValidationRequired;\n }\n\n set userAgentValidationRequired(value: boolean) {\n this.sessionConfiguration.userAgentValidationRequired = value;\n }\n\n get basicTokenLifespan() {\n return this.authConfiguration.tenantOptions['oauth.internal']['basic-token.lifespan.seconds'];\n }\n\n set basicTokenLifespan(value) {\n this.authConfiguration.tenantOptions['oauth.internal']['basic-token.lifespan.seconds'] = value;\n }\n\n get useSessionConfiguration() {\n return !!this.sessionConfiguration;\n }\n\n set useSessionConfiguration(value) {\n this.sessionConfiguration = value\n ? defaults({}, this.originalSessionConfiguration, {\n absoluteTimeoutMillis: 1209600000, // 14 days\n renewalTimeoutMillis: 86400000, // 1 day\n maximumNumberOfParallelSessions: 5,\n userAgentValidationRequired: false\n })\n : null;\n this.basicTokenLifespan = this.previousTokenLifespan || 172800; // 2 days\n }\n\n get absoluteTimeoutConstraints() {\n return {\n min: Math.max(this.MIN_ABSOLUTE_TIMEOUT, this.basicTokenLifespan + 1)\n };\n }\n\n get renewalTimeoutConstraints() {\n return {\n min: this.MIN_ABSOLUTE_TIMEOUT / 2,\n max: this.basicTokenLifespan ? this.basicTokenLifespan - 1 : null\n };\n }\n\n get basicTokenLifespanConstraints() {\n return {\n min: this.renewalTimeoutSeconds ? this.renewalTimeoutSeconds + 1 : null,\n max: this.absoluteTimeoutSeconds ? this.absoluteTimeoutSeconds - 1 : null\n };\n }\n\n get sessionConfiguration(): ISessionConfiguration {\n return this.authConfiguration.loginOptions.find(isOauthInternal).sessionConfiguration;\n }\n\n set sessionConfiguration(value: ISessionConfiguration) {\n this.authConfiguration.loginOptions.find(isOauthInternal).sessionConfiguration = value;\n }\n\n private convertToMillis(seconds: number): number {\n return isFinite(seconds) ? seconds * 1000 : null;\n }\n\n private convertToSeconds(milliseconds: number): number {\n return isFinite(milliseconds) ? Math.ceil(milliseconds / 1000) : null;\n }\n}\n","<div\n class=\"card-block separator-top overflow-auto\"\n *ngIf=\"authConfiguration.preferredLoginOptionType === tenantLoginOptionTypeEnum.OAUTH2_INTERNAL\"\n>\n <div class=\"col-sm-2\">\n <div class=\"h4 text-normal text-right text-left-xs\">\n {{ 'OAI-Secure session configuration' | translate }}\n </div>\n </div>\n\n <div class=\"col-sm-9\">\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label class=\"c8y-switch\" title=\"{{ 'Use session configuration' | translate }}\">\n <input\n type=\"checkbox\"\n name=\"useSessionConfiguration\"\n [(ngModel)]=\"useSessionConfiguration\"\n />\n <span></span>\n <span>{{ 'Use session configuration' | translate }}</span>\n </label>\n </c8y-form-group>\n </div>\n </div>\n\n <fieldset *ngIf=\"sessionConfiguration\">\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label class=\"c8y-switch\" title=\"{{ 'User agent validation required' | translate }}\">\n <input\n type=\"checkbox\"\n name=\"userAgentValidationRequired\"\n [(ngModel)]=\"userAgentValidationRequired\"\n />\n <span></span>\n <span>{{ 'User agent validation required' | translate }}</span>\n <button\n class=\"btn-help btn-help--sm\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ USER_AGENT_VALIDATION_REQUIRED_POPOVER | translate }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n ></button>\n </label>\n </c8y-form-group>\n </div>\n </div>\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label title=\"{{ 'Session absolute timeout' | translate }}\">\n {{ 'Session absolute timeout' | translate }}\n </label>\n <div class=\"input-group\">\n <input\n type=\"number\"\n name=\"absoluteTimeoutSeconds\"\n class=\"form-control text-right\"\n [(ngModel)]=\"absoluteTimeoutSeconds\"\n [required]=\"useSessionConfiguration\"\n [min]=\"absoluteTimeoutConstraints.min\"\n step=\"1\"\n />\n <span class=\"input-group-addon\" translate>seconds</span>\n </div>\n <c8y-messages>\n <c8y-message\n name=\"min\"\n text=\"{{ ABSOLUTE_TIMEOUT_VALIDATION_MESSAGE | translate }}\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n </div>\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label title=\"{{ 'Session renewal timeout' | translate }}\">\n {{ 'Session renewal timeout' | translate }}\n </label>\n <div class=\"input-group\">\n <input\n type=\"number\"\n name=\"renewalTimeoutSeconds\"\n class=\"form-control text-right\"\n [(ngModel)]=\"renewalTimeoutSeconds\"\n [required]=\"useSessionConfiguration\"\n [max]=\"renewalTimeoutConstraints.max\"\n [min]=\"renewalTimeoutConstraints.min\"\n step=\"1\"\n />\n <span class=\"input-group-addon\" translate>seconds</span>\n </div>\n <c8y-messages>\n <c8y-message\n name=\"max\"\n text=\"{{ RENEWAL_TIMEOUT_VALIDATION_MESSAGE | translate }}\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n </div>\n </div>\n\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label title=\"{{ 'Maximum parallel sessions per user' | translate }}\">\n {{ 'Maximum parallel sessions per user' | translate }}\n </label>\n <div class=\"input-group\">\n <input\n type=\"number\"\n name=\"maximumNumberOfParallelSessions\"\n class=\"form-control text-right\"\n [(ngModel)]=\"maximumNumberOfParallelSessions\"\n [required]=\"useSessionConfiguration\"\n [min]=\"1\"\n step=\"1\"\n />\n <span class=\"input-group-addon\" translate>sessions</span>\n </div>\n </c8y-form-group>\n </div>\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label title=\"{{ 'Token lifespan' | translate }}\">\n {{ 'Token lifespan' | translate }}\n </label>\n <div class=\"input-group\">\n <input\n type=\"number\"\n name=\"basicTokenLifespan\"\n class=\"form-control text-right\"\n [(ngModel)]=\"basicTokenLifespan\"\n [required]=\"useSessionConfiguration\"\n [max]=\"basicTokenLifespanConstraints.max\"\n [min]=\"basicTokenLifespanConstraints.min\"\n step=\"1\"\n />\n <span class=\"input-group-addon\" translate>seconds</span>\n </div>\n <c8y-messages>\n <c8y-message\n name=\"max\"\n text=\"{{ MAX_TOKEN_LIFESPAN_VALIDATION_MESSAGE | translate }}\"\n ></c8y-message>\n <c8y-message\n name=\"min\"\n text=\"{{ MIN_TOKEN_LIFESPAN_VALIDATION_MESSAGE | translate }}\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n </div>\n </div>\n </fieldset>\n </div>\n</div>\n","import { Component, Input, SimpleChanges } from '@angular/core';\nimport { gettext } from '@c8y/ngx-components';\nimport { ControlContainer, NgForm } from '@angular/forms';\nimport { AuthConfiguration } from './auth-configuration.model';\nimport { TenantUiService } from '@c8y/ngx-components';\nimport { TenantLoginOptionType } from '@c8y/client';\n\n@Component({\n selector: 'c8y-login-settings',\n templateUrl: './login-settings.component.html',\n viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]\n})\nexport class LoginSettingsComponent {\n PREFERRED_LOGIN_MODE_POPOVER = gettext(\n 'Main difference is the storage of the authentication information. With Basic Auth, it is saved in a session storage and with OAI-Secure in a HttpOnly cookie. OAI-Secure grant is recommended as the authentication information is not accessible via JavaScript. Single sign-on redirect allows a user to login with a single 3rd-party authorization server using the OAuth2 protocol.'\n );\n ENFORCED_BY_PLATFORM_POPOVER = gettext('The setting is enforced on the platform level.');\n IGNORE_CASE_SENSITIVITY_POPOVER = gettext(\n 'If selected, the letter case of the username does not matter during login.'\n );\n\n @Input()\n authConfiguration: AuthConfiguration;\n\n isOauth2: boolean;\n tenantLoginOptionTypeEnum = TenantLoginOptionType;\n\n private PASSWORD_CATEGORY = 'password';\n private LIMIT_VALIDITY_KEY = 'limit.validity';\n private TENANT_STRENGTH_VALIDITY_KEY = 'strength.validity';\n private SYSTEM_STRENGTH_VALIDITY_KEY = 'enforce.strength';\n\n constructor(private tenantUiService: TenantUiService) {}\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes.authConfiguration && changes.authConfiguration.currentValue) {\n this.isOauth2 = !!changes.authConfiguration.currentValue.loginOptions.find(\n this.tenantUiService.isOauth2\n );\n }\n }\n\n get systemPasswordLimitValidity() {\n return this.authConfiguration.systemOptions[this.PASSWORD_CATEGORY][this.LIMIT_VALIDITY_KEY];\n }\n\n get passwordLimitValidity() {\n return this.systemPasswordLimitValidity !== null\n ? this.systemPasswordLimitValidity\n : this.authConfiguration.tenantOptions[this.PASSWORD_CATEGORY][this.LIMIT_VALIDITY_KEY];\n }\n\n set passwordLimitValidity(value) {\n if (this.systemPasswordLimitValidity === null) {\n this.authConfiguration.tenantOptions[this.PASSWORD_CATEGORY][this.LIMIT_VALIDITY_KEY] = value;\n }\n }\n\n get systemPasswordEnforceStrength() {\n return this.authConfiguration.systemOptions[this.PASSWORD_CATEGORY][\n this.SYSTEM_STRENGTH_VALIDITY_KEY\n ];\n }\n\n get passwordEnforceStrength() {\n return this.systemPasswordEnforceStrength\n ? this.systemPasswordEnforceStrength\n : this.authConfiguration.tenantOptions[this.PASSWORD_CATEGORY][\n this.TENANT_STRENGTH_VALIDITY_KEY\n ];\n }\n\n set passwordEnforceStrength(value) {\n if (!this.systemPasswordEnforceStrength) {\n this.authConfiguration.tenantOptions[this.PASSWORD_CATEGORY][\n this.TENANT_STRENGTH_VALIDITY_KEY\n ] = value;\n }\n }\n\n get tenantLoginIgnoreCase() {\n return this.authConfiguration.tenantOptions['configuration']['tenant.login.ignore-case'];\n }\n\n set tenantLoginIgnoreCase(value) {\n this.authConfiguration.tenantOptions['configuration']['tenant.login.ignore-case'] = value;\n }\n}\n","<div class=\"card-block separator-top overflow-auto\" *ngIf=\"authConfiguration\">\n <div class=\"col-sm-2\">\n <div class=\"h4 text-normal text-right text-left-xs\">{{ 'Login settings' | translate }}</div>\n </div>\n\n <div class=\"col-sm-9\">\n <div class=\"row m-b-8\">\n <c8y-form-group class=\"col-sm-6\">\n <label>\n {{ 'Preferred login mode' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ PREFERRED_LOGIN_MODE_POPOVER | translate }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n ></button>\n </label>\n <div class=\"c8y-select-wrapper\">\n <select\n [attr.aria-label]=\"'Auth type' | translate\"\n class=\"form-control\"\n id=\"preferredLoginOptionType\"\n name=\"preferredLoginOptionType\"\n [(ngModel)]=\"authConfiguration.preferredLoginOptionType\"\n >\n <option value=\"{{ tenantLoginOptionTypeEnum.BASIC }}\" translate>Basic Auth</option>\n <option value=\"{{ tenantLoginOptionTypeEnum.OAUTH2_INTERNAL }}\" translate>\n OAI-Secure\n </option>\n <option value=\"{{ tenantLoginOptionTypeEnum.OAUTH2 }}\" [disabled]=\"!isOauth2\" translate>\n Single sign-on redirect\n </option>\n </select>\n </div>\n </c8y-form-group>\n </div>\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label title=\"{{ 'Password validity limit' | translate }}\">\n {{ 'Password validity limit' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ ENFORCED_BY_PLATFORM_POPOVER | translate }}\"\n placement=\"bottom\"\n triggers=\"focus\"\n *ngIf=\"systemPasswordLimitValidity\"\n ></button>\n </label>\n <div class=\"input-group\">\n <input\n type=\"number\"\n name=\"passwordLimitValidity\"\n class=\"form-control text-right\"\n [(ngModel)]=\"passwordLimitValidity\"\n min=\"0\"\n max=\"999999\"\n step=\"1\"\n required\n [disabled]=\"systemPasswordLimitValidity\"\n />\n <span class=\"input-group-addon\" translate>days</span>\n </div>\n <p class=\"help-block\">\n {{ 'Default: 0 (unlimited validity)' | translate }}\n