From 22e993f44c129c30b73a426089c08f08db8c348b Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:27:59 +0100 Subject: [PATCH] V15: Change password should not be shown when local login is disabled (#17900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: include the option whether a login provider has disabled local login * feat: include information whether change password or two factor is allowed * generate new OpenApi.json * generate new ts client * feat: request the server configuration and include in the UmbAppContext * fix: for login screen, check that a provider did not disable the local login * fix: use UmbAppContext to fetch the version check period * chore: remove unused method * revert current user configuration changes * generate new ts client * fix: add condition for "allow password change" on the change password button * fix: create new IsDefaultKind condition to separate change password logic * fix: update "allow change password" and "allow mfa" conditions to take the user configuration into consideration * chore: export consts * remove falsely named attribute that happens to not be useful anyway * chore: revamp logic for early return to make it more readable * convert `isInitialized` to a Promise as it is only being resolved once anyway, which then additionally saves a call to `this.observe` in the conditions --------- Co-authored-by: Niels Lyngsø --- .../Server/ConfigurationServerController.cs | 16 +++++- .../Factories/UserPresentationFactory.cs | 4 ++ src/Umbraco.Cms.Api.Management/OpenApi.json | 22 ++++--- .../ServerConfigurationResponseModel.cs | 2 + .../User/UserConfigurationResponseModel.cs | 4 ++ .../apps/app/app-context-config.interface.ts | 8 +++ .../src/apps/app/app.context.ts | 6 ++ .../src/apps/app/app.element.ts | 6 +- .../src/apps/app/server-connection.ts | 23 +++++++- .../src/external/backend-api/src/sdk.gen.ts | 5 +- .../src/external/backend-api/src/types.gen.ts | 7 ++- .../src/mocks/handlers/server.handlers.ts | 1 + .../auth/modals/umb-app-auth-modal.element.ts | 57 ++++++++++++++++--- .../sysinfo/repository/sysinfo.repository.ts | 21 ++----- .../src/packages/sysinfo/types.ts | 6 +- .../entity-action/manifests.ts | 3 + .../modals/external-login-modal.element.ts | 1 - .../user/current-user/profile/manifests.ts | 5 ++ ...-allow-change-password-action.condition.ts | 33 +++++++---- .../user-allow-mfa-action.condition.ts | 21 +++++-- .../user/user/conditions/constants.ts | 1 + .../conditions/is-default-kind/constants.ts | 1 + .../conditions/is-default-kind/manifests.ts | 10 ++++ .../user-is-default-kind.condition.ts | 15 +++++ .../user/user/conditions/manifests.ts | 2 + .../config/user-config.repository.ts | 15 ++++- .../utils/all-umb-consts/index.ts | 2 +- 27 files changed, 228 insertions(+), 69 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/user-is-default-kind.condition.ts diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Server/ConfigurationServerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Server/ConfigurationServerController.cs index 048ed55206..05c35f32cb 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Server/ConfigurationServerController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Server/ConfigurationServerController.cs @@ -1,8 +1,10 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Management.Security; using Umbraco.Cms.Api.Management.ViewModels.Server; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; @@ -14,6 +16,7 @@ public class ConfigurationServerController : ServerControllerBase { private readonly SecuritySettings _securitySettings; private readonly GlobalSettings _globalSettings; + private readonly IBackOfficeExternalLoginProviders _externalLoginProviders; [Obsolete("Use the constructor that accepts all arguments. Will be removed in V16.")] public ConfigurationServerController(IOptions securitySettings) @@ -21,13 +24,21 @@ public class ConfigurationServerController : ServerControllerBase { } - [ActivatorUtilitiesConstructor] + [Obsolete("Use the constructor that accepts all arguments. Will be removed in V16.")] public ConfigurationServerController(IOptions securitySettings, IOptions globalSettings) + : this(securitySettings, globalSettings, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [ActivatorUtilitiesConstructor] + public ConfigurationServerController(IOptions securitySettings, IOptions globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders) { _securitySettings = securitySettings.Value; _globalSettings = globalSettings.Value; + _externalLoginProviders = externalLoginProviders; } + [AllowAnonymous] [HttpGet("configuration")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(ServerConfigurationResponseModel), StatusCodes.Status200OK)] @@ -36,7 +47,8 @@ public class ConfigurationServerController : ServerControllerBase var responseModel = new ServerConfigurationResponseModel { AllowPasswordReset = _securitySettings.AllowPasswordReset, - VersionCheckPeriod = _globalSettings.VersionCheckPeriod + VersionCheckPeriod = _globalSettings.VersionCheckPeriod, + AllowLocalLogin = _externalLoginProviders.HasDenyLocalLogin() is false, }; return Task.FromResult(Ok(responseModel)); diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs index 7ba1229416..0b7867a812 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs @@ -155,6 +155,10 @@ public class UserPresentationFactory : IUserPresentationFactory CanInviteUsers = _emailSender.CanSendRequiredEmail() && _externalLoginProviders.HasDenyLocalLogin() is false, UsernameIsEmail = _securitySettings.UsernameIsEmail, PasswordConfiguration = _passwordConfigurationPresentationFactory.CreatePasswordConfigurationResponseModel(), + + // You should not be able to change any password or set 2fa if any providers has deny local login set. + AllowChangePassword = _externalLoginProviders.HasDenyLocalLogin() is false, + AllowTwoFactor = _externalLoginProviders.HasDenyLocalLogin() is false, }); public async Task CreateUpdateModelAsync(Guid existingUserKey, UpdateUserRequestModel updateModel) diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 7042802f04..0da041cea4 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -25276,16 +25276,8 @@ } } } - }, - "401": { - "description": "The resource is protected and requires an authentication token" } - }, - "security": [ - { - "Backoffice User": [ ] - } - ] + } } }, "/umbraco/management/api/v1/server/information": { @@ -43250,6 +43242,7 @@ }, "ServerConfigurationResponseModel": { "required": [ + "allowLocalLogin", "allowPasswordReset", "versionCheckPeriod" ], @@ -43261,6 +43254,9 @@ "versionCheckPeriod": { "type": "integer", "format": "int32" + }, + "allowLocalLogin": { + "type": "boolean" } }, "additionalProperties": false @@ -45379,6 +45375,8 @@ }, "UserConfigurationResponseModel": { "required": [ + "allowChangePassword", + "allowTwoFactor", "canInviteUsers", "passwordConfiguration", "usernameIsEmail" @@ -45397,6 +45395,12 @@ "$ref": "#/components/schemas/PasswordConfigurationResponseModel" } ] + }, + "allowChangePassword": { + "type": "boolean" + }, + "allowTwoFactor": { + "type": "boolean" } }, "additionalProperties": false diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Server/ServerConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Server/ServerConfigurationResponseModel.cs index a424a24798..9e8ef9fe7e 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Server/ServerConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Server/ServerConfigurationResponseModel.cs @@ -5,4 +5,6 @@ public class ServerConfigurationResponseModel public bool AllowPasswordReset { get; set; } public int VersionCheckPeriod { get; set; } + + public bool AllowLocalLogin { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/UserConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/UserConfigurationResponseModel.cs index a64cb4273c..7f64bd08e3 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/UserConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/UserConfigurationResponseModel.cs @@ -9,4 +9,8 @@ public class UserConfigurationResponseModel public bool UsernameIsEmail { get; set; } public required PasswordConfigurationResponseModel PasswordConfiguration { get; set; } + + public bool AllowChangePassword { get; set; } + + public bool AllowTwoFactor { get; set; } } diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-context-config.interface.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-context-config.interface.ts index ede076ea25..ede2d780f1 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app-context-config.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-context-config.interface.ts @@ -1,3 +1,5 @@ +import type { UmbServerConnection } from './server-connection.js'; + /** * Configuration interface for the Umbraco App Context * @interface UmbAppContextConfig @@ -16,4 +18,10 @@ export interface UmbAppContextConfig { * @memberof UmbAppContextConfig */ backofficePath: string; + + /** + * Configuration for the server connection. + * @memberof UmbAppContextConfig + */ + serverConnection: UmbServerConnection; } diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts index 39a248c8f5..857064431b 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts @@ -6,11 +6,13 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; export class UmbAppContext extends UmbContextBase { #serverUrl: string; #backofficePath: string; + #serverConnection; constructor(host: UmbControllerHost, config: UmbAppContextConfig) { super(host, UMB_APP_CONTEXT); this.#serverUrl = config.serverUrl; this.#backofficePath = config.backofficePath; + this.#serverConnection = config.serverConnection; } getBackofficePath() { @@ -20,6 +22,10 @@ export class UmbAppContext extends UmbContextBase { getServerUrl() { return this.#serverUrl; } + + getServerConnection() { + return this.#serverConnection; + } } export const UMB_APP_CONTEXT = new UmbContextToken('UmbAppContext'); diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts index 3be7df953e..e77efa374b 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts @@ -163,7 +163,11 @@ export class UmbAppElement extends UmbLitElement { this.#serverConnection = await new UmbServerConnection(this.serverUrl).connect(); this.#authContext = new UmbAuthContext(this, this.serverUrl, this.backofficePath, this.bypassAuth); - new UmbAppContext(this, { backofficePath: this.backofficePath, serverUrl: this.serverUrl }); + new UmbAppContext(this, { + backofficePath: this.backofficePath, + serverUrl: this.serverUrl, + serverConnection: this.#serverConnection, + }); // Register Core extensions (this is specifically done here because we need these extensions to be registered before the application is initialized) onInit(this, umbExtensionsRegistry); diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/server-connection.ts b/src/Umbraco.Web.UI.Client/src/apps/app/server-connection.ts index 6f30829184..4365b643f0 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/server-connection.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/server-connection.ts @@ -1,5 +1,5 @@ import { RuntimeLevelModel, ServerService } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbBooleanState, UmbNumberState } from '@umbraco-cms/backoffice/observable-api'; import { tryExecute } from '@umbraco-cms/backoffice/resources'; export class UmbServerConnection { @@ -9,6 +9,15 @@ export class UmbServerConnection { #isConnected = new UmbBooleanState(false); isConnected = this.#isConnected.asObservable(); + #versionCheckPeriod = new UmbNumberState(undefined); + versionCheckPeriod = this.#versionCheckPeriod.asObservable(); + + #allowLocalLogin = new UmbBooleanState(false); + allowLocalLogin = this.#allowLocalLogin.asObservable(); + + #allowPasswordReset = new UmbBooleanState(false); + allowPasswordReset = this.#allowPasswordReset.asObservable(); + constructor(serverUrl: string) { this.#url = serverUrl; } @@ -19,6 +28,7 @@ export class UmbServerConnection { */ async connect() { await this.#setStatus(); + await this.#setServerConfiguration(); return this; } @@ -59,4 +69,15 @@ export class UmbServerConnection { this.#isConnected.setValue(true); this.#status = data?.serverStatus ?? RuntimeLevelModel.UNKNOWN; } + + async #setServerConfiguration() { + const { data, error } = await tryExecute(ServerService.getServerConfiguration()); + if (error) { + throw error; + } + + this.#versionCheckPeriod.setValue(data?.versionCheckPeriod); + this.#allowLocalLogin.setValue(data?.allowLocalLogin ?? false); + this.#allowPasswordReset.setValue(data?.allowPasswordReset ?? false); + } } diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/sdk.gen.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/sdk.gen.ts index c3f1d924dc..40b4e3a592 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/sdk.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/sdk.gen.ts @@ -6685,10 +6685,7 @@ export class ServerService { public static getServerConfiguration(): CancelablePromise { return __request(OpenAPI, { method: 'GET', - url: '/umbraco/management/api/v1/server/configuration', - errors: { - 401: 'The resource is protected and requires an authentication token' - } + url: '/umbraco/management/api/v1/server/configuration' }); } diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts index 476aa305f9..6ededd6264 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts @@ -1027,8 +1027,8 @@ export type HealthCheckWithResultPresentationModel = { export enum HealthStatusModel { HEALTHY = 'Healthy', UNHEALTHY = 'Unhealthy', - CORRUPT = 'Corrupt', - REBUILDING = 'Rebuilding' + REBUILDING = 'Rebuilding', + CORRUPT = 'Corrupt' } export type HealthStatusResponseModel = { @@ -2204,6 +2204,7 @@ export type ServerConfigurationItemResponseModel = { export type ServerConfigurationResponseModel = { allowPasswordReset: boolean; versionCheckPeriod: number; + allowLocalLogin: boolean; }; export type ServerInformationResponseModel = { @@ -2681,6 +2682,8 @@ export type UserConfigurationResponseModel = { canInviteUsers: boolean; usernameIsEmail: boolean; passwordConfiguration: (PasswordConfigurationResponseModel); + allowChangePassword: boolean; + allowTwoFactor: boolean; }; export type UserDataModel = { diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/server.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/server.handlers.ts index 668b2cb718..b635eb3b90 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/server.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/server.handlers.ts @@ -49,6 +49,7 @@ export const serverInformationHandlers = [ ctx.json({ allowPasswordReset: true, versionCheckPeriod: 7, // days + allowLocalLogin: true, }), ); }), diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts index 2082e50550..a92627d95d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts @@ -4,7 +4,7 @@ import { UmbTextStyles } from '../../style/text-style.style.js'; import { UMB_AUTH_CONTEXT } from '../auth.context.token.js'; import type { UmbAuthProviderDefaultProps } from '../types.js'; import type { UmbModalAppAuthConfig, UmbModalAppAuthValue } from './umb-app-auth-modal.token.js'; -import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; @customElement('umb-app-auth-modal') @@ -15,6 +15,12 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement { this._serverUrl = context.getServerUrl(); this.style.setProperty( '--image', `url('${this._serverUrl}/umbraco/management/api/v1/security/back-office/graphics/login-background') no-repeat center center/cover`, ); + + const serverConnection = context.getServerConnection(); + + this.observe(serverConnection.allowLocalLogin, (allowLocalLogin) => { + this._allowLocalLogin = allowLocalLogin; + }); + + this.observe(serverConnection.isConnected, (isConnected) => { + this._loading = !isConnected; + }); }); } @@ -93,17 +107,36 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement${this.localize.term('login_timeout')}

` : ''} - + ${when( + this._loading, + () => html` +
+ +
+ `, + () => + html` `, + )} `; } + #filterProvider = (provider: ManifestAuthProvider) => { + if (this._allowLocalLogin) { + return true; + } + + // Do not show any Umbraco auth provider if local login is disabled + return provider.forProviderName.toLowerCase() !== 'umbraco'; + }; + private onSubmit = async (providerOrManifest: string | ManifestAuthProvider, loginHint?: string) => { try { const authContext = await this.getContext(UMB_AUTH_CONTEXT); @@ -231,6 +264,12 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement { // Check if we are allowed to check again - const versionCheckPeriod = await this.#getVersionCheckPeriod(); + const appContext = await this.getContext(UMB_APP_CONTEXT); + const versionCheckPeriod = await this.observe(appContext.getServerConnection().versionCheckPeriod).asPromise(); if (versionCheckPeriod <= 0) { return null; @@ -77,14 +72,6 @@ export class UmbSysinfoRepository extends UmbRepositoryBase { return null; } - async #getVersionCheckPeriod(): Promise { - if (!this.#serverConfiguration) { - this.#serverConfiguration = await this.requestServerConfiguration(); - } - - return this.#serverConfiguration?.versionCheckPeriod ?? 7; - } - #getStoredServerUpgradeCheck(lastCheck: Date): UmbServerUpgradeCheck | null { const storedCheck = localStorage.getItem('umb:serverUpgradeCheck'); if (storedCheck) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/sysinfo/types.ts b/src/Umbraco.Web.UI.Client/src/packages/sysinfo/types.ts index d4a6519664..7babfafeb4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/sysinfo/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/sysinfo/types.ts @@ -1,7 +1,3 @@ -import type { - ServerConfigurationResponseModel, - UpgradeCheckResponseModel, -} from '@umbraco-cms/backoffice/external/backend-api'; +import type { UpgradeCheckResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -export type UmbServerConfiguration = ServerConfigurationResponseModel; export type UmbServerUpgradeCheck = UpgradeCheckResponseModel & { expires: string }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/change-password/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/change-password/entity-action/manifests.ts index 01148ac695..dce4333405 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/change-password/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/change-password/entity-action/manifests.ts @@ -14,6 +14,9 @@ export const manifests: Array = [ label: '#user_changePassword', }, conditions: [ + { + alias: 'Umb.Condition.User.IsDefaultKind', + }, { alias: 'Umb.Condition.User.AllowChangePassword', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index c5f2d1c0a2..a6c7cf07c9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -144,7 +144,6 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { this.#onProviderEnable(item)}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/profile/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/profile/manifests.ts index 0d088f3dd0..3893b85d47 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/profile/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/profile/manifests.ts @@ -39,5 +39,10 @@ export const manifests: Array = [ label: '#general_changePassword', icon: 'lock', }, + conditions: [ + { + alias: 'Umb.Condition.User.AllowChangePassword', + }, + ], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/user-allow-change-password-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/user-allow-change-password-action.condition.ts index d82ad989ae..6d2176da44 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/user-allow-change-password-action.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/user-allow-change-password-action.condition.ts @@ -1,14 +1,27 @@ -import { UmbUserKind } from '../../utils/index.js'; -import { UmbUserActionConditionBase } from '../user-allow-action-base.condition.js'; +import UmbUserConfigRepository from '../../repository/config/user-config.repository.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; +import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; -export class UmbUserAllowChangePasswordActionCondition extends UmbUserActionConditionBase { - async _onUserDataChange() { - // don't allow the current user to delete themselves - if (this.userKind === UmbUserKind.DEFAULT) { - this.permitted = true; - } else { - this.permitted = false; - } +export class UmbUserAllowChangePasswordActionCondition extends UmbConditionBase { + #configRepository = new UmbUserConfigRepository(this._host); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(host: UmbControllerHost, args: any) { + super(host, args); + this.#init(); + } + + async #init() { + await this.#configRepository.initialized; + + this.observe( + this.#configRepository.part('allowChangePassword'), + (isAllowed) => { + this.permitted = isAllowed; + }, + '_userAllowChangePasswordActionCondition', + ); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/user-allow-mfa-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/user-allow-mfa-action.condition.ts index 88bd9f4274..b1d37a1e24 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/user-allow-mfa-action.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/user-allow-mfa-action.condition.ts @@ -1,16 +1,29 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbConditionBase, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { UmbUserConfigRepository } from '../../repository/config/index.js'; export class UmbUserAllowMfaActionCondition extends UmbConditionBase { + #configRepository = new UmbUserConfigRepository(this._host); + // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(host: UmbControllerHost, args: any) { super(host, args); + this.#init(); + } + + async #init() { + await this.#configRepository.initialized; - // Check if there are any MFA providers available this.observe( - umbExtensionsRegistry.byType('mfaLoginProvider'), - (exts) => (this.permitted = exts.length > 0), - '_userAllowMfaActionConditionProviders', + observeMultiple([ + this.#configRepository.part('allowTwoFactor'), + umbExtensionsRegistry.byType('mfaLoginProvider'), + ]), + ([allowTwoFactor, exts]) => { + this.permitted = allowTwoFactor && exts.length > 0; + }, + '_userAllowMfaActionCondition', ); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/constants.ts index 72e5bc2cc7..518d532df2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/constants.ts @@ -5,3 +5,4 @@ export * from './allow-enable/constants.js'; export * from './allow-external-login/constants.js'; export * from './allow-mfa/constants.js'; export * from './allow-unlock/constants.js'; +export * from './is-default-kind/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/constants.ts new file mode 100644 index 0000000000..71364d3ae7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/constants.ts @@ -0,0 +1 @@ +export const UMB_USER_IS_DEFAULT_KIND_CONDITION_ALIAS = 'Umb.Condition.User.IsDefaultKind'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/manifests.ts new file mode 100644 index 0000000000..1c25a7658e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/manifests.ts @@ -0,0 +1,10 @@ +import { UMB_USER_IS_DEFAULT_KIND_CONDITION_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'condition', + name: 'User Is Default Kind Condition', + alias: UMB_USER_IS_DEFAULT_KIND_CONDITION_ALIAS, + api: () => import('./user-is-default-kind.condition.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/user-is-default-kind.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/user-is-default-kind.condition.ts new file mode 100644 index 0000000000..086e72c181 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/is-default-kind/user-is-default-kind.condition.ts @@ -0,0 +1,15 @@ +import { UmbUserKind } from '../../utils/index.js'; +import { UmbUserActionConditionBase } from '../user-allow-action-base.condition.js'; + +export class UmbUserIsDefaultKindCondition extends UmbUserActionConditionBase { + async _onUserDataChange() { + // don't allow the current user to delete themselves + if (this.userKind === UmbUserKind.DEFAULT) { + this.permitted = true; + } else { + this.permitted = false; + } + } +} + +export { UmbUserIsDefaultKindCondition as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts index 32277cea4e..3e4f1ee52a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts @@ -5,6 +5,7 @@ import { manifests as userAllowEnableActionManifests } from './allow-enable/mani import { manifests as userAllowExternalLoginActionManifests } from './allow-external-login/manifests.js'; import { manifests as userAllowMfaActionManifests } from './allow-mfa/manifests.js'; import { manifests as userAllowUnlockActionManifests } from './allow-unlock/manifests.js'; +import { manifests as userIsDefaultKindManifests } from './is-default-kind/manifests.js'; export const manifests: Array = [ ...userAllowChangePasswordActionManifests, @@ -14,4 +15,5 @@ export const manifests: Array = [ ...userAllowExternalLoginActionManifests, ...userAllowMfaActionManifests, ...userAllowUnlockActionManifests, + ...userIsDefaultKindManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/user-config.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/user-config.repository.ts index 082ab74f2d..ac3b5b95eb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/user-config.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/user-config.repository.ts @@ -7,14 +7,23 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { Observable } from '@umbraco-cms/backoffice/observable-api'; export class UmbUserConfigRepository extends UmbRepositoryBase implements UmbApi { + /** + * Promise that resolves when the repository has been initialized, i.e. when the user configuration has been fetched from the server. + * @memberof UmbUserConfigRepository + */ + initialized: Promise; + #dataStore?: typeof UMB_USER_CONFIG_STORE_CONTEXT.TYPE; #dataSource = new UmbUserConfigServerDataSource(this); constructor(host: UmbControllerHost) { super(host); - this.consumeContext(UMB_USER_CONFIG_STORE_CONTEXT, (store) => { - this.#dataStore = store; - this.#init(); + this.initialized = new Promise((resolve) => { + this.consumeContext(UMB_USER_CONFIG_STORE_CONTEXT, async (store) => { + this.#dataStore = store; + await this.#init(); + resolve(); + }); }); } diff --git a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts index ab031de6e6..168782f0df 100644 --- a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts +++ b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts @@ -400,7 +400,7 @@ export const foundConsts = [{ }, { path: '@umbraco-cms/backoffice/user', - consts: ["UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL","UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS","UMB_USER_CLIENT_CREDENTIAL_REPOSITORY_ALIAS","UMB_USER_COLLECTION_ALIAS","UMB_USER_COLLECTION_REPOSITORY_ALIAS","UMB_USER_COLLECTION_CONTEXT","UMB_COLLECTION_VIEW_USER_TABLE","UMB_COLLECTION_VIEW_USER_GRID","UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS","UMB_USER_ALLOW_DELETE_CONDITION_ALIAS","UMB_USER_ALLOW_DISABLE_CONDITION_ALIAS","UMB_USER_ALLOW_ENABLE_CONDITION_ALIAS","UMB_USER_ALLOW_EXTERNAL_LOGIN_CONDITION_ALIAS","UMB_USER_ALLOW_MFA_CONDITION_ALIAS","UMB_USER_ALLOW_UNLOCK_CONDITION_ALIAS","UMB_CREATE_USER_MODAL","UMB_CREATE_USER_SUCCESS_MODAL","UMB_CREATE_USER_MODAL_ALIAS","UMB_USER_ENTITY_TYPE","UMB_USER_ROOT_ENTITY_TYPE","UMB_INVITE_USER_MODAL","UMB_RESEND_INVITE_TO_USER_MODAL","UMB_INVITE_USER_REPOSITORY_ALIAS","UMB_USER_MFA_MODAL","UMB_USER_PICKER_MODAL","UMB_USER_WORKSPACE_PATH","UMB_USER_ROOT_WORKSPACE_PATH","UMB_USER_AVATAR_REPOSITORY_ALIAS","UMB_CHANGE_USER_PASSWORD_REPOSITORY_ALIAS","UMB_USER_CONFIG_REPOSITORY_ALIAS","UMB_USER_CONFIG_STORE_ALIAS","UMB_USER_CONFIG_STORE_CONTEXT","UMB_USER_DETAIL_REPOSITORY_ALIAS","UMB_USER_DETAIL_STORE_ALIAS","UMB_USER_DETAIL_STORE_CONTEXT","UMB_DISABLE_USER_REPOSITORY_ALIAS","UMB_ENABLE_USER_REPOSITORY_ALIAS","UMB_USER_ITEM_REPOSITORY_ALIAS","UMB_USER_ITEM_STORE_ALIAS","UMB_USER_ITEM_STORE_CONTEXT","UMB_NEW_USER_PASSWORD_REPOSITORY_ALIAS","UMB_UNLOCK_USER_REPOSITORY_ALIAS","UMB_USER_WORKSPACE_ALIAS","UMB_USER_WORKSPACE_CONTEXT","UMB_USER_ROOT_WORKSPACE_ALIAS"] + consts: ["UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL","UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS","UMB_USER_CLIENT_CREDENTIAL_REPOSITORY_ALIAS","UMB_USER_COLLECTION_ALIAS","UMB_USER_COLLECTION_REPOSITORY_ALIAS","UMB_USER_COLLECTION_CONTEXT","UMB_COLLECTION_VIEW_USER_TABLE","UMB_COLLECTION_VIEW_USER_GRID","UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS","UMB_USER_ALLOW_DELETE_CONDITION_ALIAS","UMB_USER_ALLOW_DISABLE_CONDITION_ALIAS","UMB_USER_ALLOW_ENABLE_CONDITION_ALIAS","UMB_USER_ALLOW_EXTERNAL_LOGIN_CONDITION_ALIAS","UMB_USER_ALLOW_MFA_CONDITION_ALIAS","UMB_USER_ALLOW_UNLOCK_CONDITION_ALIAS","UMB_USER_IS_DEFAULT_KIND_CONDITION_ALIAS","UMB_CREATE_USER_MODAL","UMB_CREATE_USER_SUCCESS_MODAL","UMB_CREATE_USER_MODAL_ALIAS","UMB_USER_ENTITY_TYPE","UMB_USER_ROOT_ENTITY_TYPE","UMB_INVITE_USER_MODAL","UMB_RESEND_INVITE_TO_USER_MODAL","UMB_INVITE_USER_REPOSITORY_ALIAS","UMB_USER_MFA_MODAL","UMB_USER_PICKER_MODAL","UMB_USER_WORKSPACE_PATH","UMB_USER_ROOT_WORKSPACE_PATH","UMB_USER_AVATAR_REPOSITORY_ALIAS","UMB_CHANGE_USER_PASSWORD_REPOSITORY_ALIAS","UMB_USER_CONFIG_REPOSITORY_ALIAS","UMB_USER_CONFIG_STORE_ALIAS","UMB_USER_CONFIG_STORE_CONTEXT","UMB_USER_DETAIL_REPOSITORY_ALIAS","UMB_USER_DETAIL_STORE_ALIAS","UMB_USER_DETAIL_STORE_CONTEXT","UMB_DISABLE_USER_REPOSITORY_ALIAS","UMB_ENABLE_USER_REPOSITORY_ALIAS","UMB_USER_ITEM_REPOSITORY_ALIAS","UMB_USER_ITEM_STORE_ALIAS","UMB_USER_ITEM_STORE_CONTEXT","UMB_NEW_USER_PASSWORD_REPOSITORY_ALIAS","UMB_UNLOCK_USER_REPOSITORY_ALIAS","UMB_USER_WORKSPACE_ALIAS","UMB_USER_WORKSPACE_CONTEXT","UMB_USER_ROOT_WORKSPACE_ALIAS"] }, { path: '@umbraco-cms/backoffice/utils',