From 146f938051de358983c44d8444c3d13c70522466 Mon Sep 17 00:00:00 2001 From: nikolajlauridsen Date: Fri, 1 Apr 2022 13:30:32 +0200 Subject: [PATCH 1/4] Do a full save on first member login --- src/Umbraco.Infrastructure/Security/MemberUserStore.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 345a404fcf..d2b0db111c 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -597,7 +597,10 @@ namespace Umbraco.Cms.Core.Security || (member.LastLoginDate != default && identityUser.LastLoginDateUtc.HasValue == false) || (identityUser.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value)) { - changeType = MemberDataChangeType.LoginOnly; + // If the LastLoginDate is default on the member we have to do a full save. + // This is because the umbraco property data for the member doesn't exist yet in this case + // meaning we can't just update that property data, but have to do a full save to create it + changeType = member.LastLoginDate == default ? MemberDataChangeType.FullSave : MemberDataChangeType.LoginOnly; // if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime DateTime dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); From 68353f9d0655830f94626813eb7e94652bbd1454 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 19 Apr 2022 08:13:24 +0200 Subject: [PATCH 2/4] Fixes RedirectToUmbracoPageResult to handle redirects to pages with domains defined on them (#12259) * Fixes RedirectToUmbracoPageResult to handle redirects to pages with domains defined on them. * Renamed variable to match with updated service type. * Apply suggestions from code review Co-authored-by: Ronald Barendse * Fixed usage of IUrlHelper. Co-authored-by: Ronald Barendse --- .../ActionResults/RedirectToUmbracoPageResult.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs index 62d0dc7a10..9cbbe91f11 100644 --- a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs +++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs @@ -2,9 +2,9 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; @@ -122,8 +122,9 @@ namespace Umbraco.Cms.Web.Website.ActionResults } HttpContext httpContext = context.HttpContext; - IIOHelper ioHelper = httpContext.RequestServices.GetRequiredService(); - string destinationUrl = ioHelper.ResolveUrl(Url); + IUrlHelperFactory urlHelperFactory = httpContext.RequestServices.GetRequiredService(); + IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(context); + string destinationUrl = urlHelper.Content(Url); if (_queryString.HasValue) { @@ -134,6 +135,5 @@ namespace Umbraco.Cms.Web.Website.ActionResults return Task.CompletedTask; } - } } From 852305b7d1219f72848f34c0d8e58b3a3c8c9e38 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 19 Apr 2022 08:33:03 +0200 Subject: [PATCH 3/4] Simplified setup of 2FA for users (#12142) * Added functionality to enable 2FA for users.. * Do not use the obsolete ctor in tests * cleanup * Cleanup * Convert User view from overlay to infinite editor * Add support for having additional editors on top of the user (2fa) which overlay does not support * Add controllerAs syntax in the template * Remove unused dependencies * Adjustments to 2fa login view * organize elements * add translations * add a11y helpers * add autocompletion = one-time-code * change to controllerAs syntax * add callback to cancel 2fa and fix error where submit button was not reset when all other validations were * add a cancel/go back button to the 2fa view * replace header with something less obstrusive * move logout button to the footer in the new editor view * change 'edit profile' to an umb-box and move ng-if for password fields out to reduce amount of checks * Add umb-box to external login provider section * add umb-box to user history section * bug: fix bug where notificationsService would not allow new notifications if removeAll had been called * add styling and a11y to configureTwoFactor view - also ensure that the view reloads when changes happen in the custom user view to enable 2fa - ensure that view updates when disabling 2fa - add extra button to show options (disable) for each 2fa provider * add notification when 2fa is disabled * add data-element to support the intro tour also changed a minor selector in the cypress test * correct usage of umb-box with umb-box-content * do not use the .form class twice to prevent double box-shadow * make tranlastion for 2fa placeholder shorter * ensure that field with 2fa provider is always visible when more than 1 provider * move error state of 2fa field to token field * update translation of multiple 2fa providers * move CTA buttons to right side to follow general UI practices * rename options to disable * add disabled state * add helper folders to gitignore so you can work with plugins and custom code without committing it accidentally * move the disable functionality to its own infinite editor view * use properties from umb-control-group correctly * add 'track by' to repeater * make use of umb-control-group * remove unused functions * clean up translations * add Danish translations * copy translations to english * Only return enabled 2fa providers as expected Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .gitignore | 2 + .../Models/UserTwoFactorProviderModel.cs | 20 + .../Services/ITwoFactorLoginService.cs | 8 + .../Security/BackOfficeUserStore.cs | 46 +- .../Security/UmbracoUserManager.cs | 4 +- .../Implement/TwoFactorLoginService.cs | 78 +++- .../Controllers/AuthenticationController.cs | 58 ++- .../Controllers/BackOfficeServerVariables.cs | 4 + .../Controllers/TwoFactorLoginController.cs | 122 +++++ .../UmbracoBuilder.BackOfficeAuth.cs | 5 + .../UmbracoBuilder.BackOfficeIdentity.cs | 5 +- .../DefaultBackOfficeTwoFactorOptions.cs | 10 + .../NoopBackOfficeTwoFactorOptions.cs | 5 +- .../Security/TwoFactorLoginViewOptions.cs | 13 + .../Security/BackOfficeUserManager.cs | 1 + .../Security/MemberManager.cs | 3 - .../application/umbappheader.directive.js | 18 +- .../application/umblogin.directive.js | 4 + .../resources/twofactorlogin.resource.js | 136 ++++++ .../common/services/notifications.service.js | 2 +- .../configuretwofactor.controller.js | 79 ++++ .../twofactor/configuretwofactor.html | 58 +++ .../twofactor/disabletwofactor.controller.js | 53 +++ .../twofactor/disabletwofactor.html | 56 +++ .../infiniteeditors/user/user.controller.js | 219 +++++++++ .../common/infiniteeditors/user/user.html | 145 ++++++ .../src/views/common/login-2fa.controller.js | 38 ++ .../src/views/common/login-2fa.html | 44 ++ .../common/overlays/user/user.controller.js | 194 -------- .../src/views/common/overlays/user/user.html | 143 ------ .../src/views/users/user.controller.js | 25 +- .../users/views/user/details.controller.js | 1 + .../src/views/users/views/user/details.html | 10 + src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 + src/Umbraco.Web.UI/umbraco/config/lang/da.xml | 33 +- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 9 + .../umbraco/config/lang/en_us.xml | 421 +++++++++--------- .../integration/Tours/backofficeTour.ts | 2 +- .../Security/BackOfficeUserStoreTests.cs | 5 +- 39 files changed, 1497 insertions(+), 586 deletions(-) create mode 100644 src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs create mode 100644 src/Umbraco.Web.BackOffice/Controllers/TwoFactorLoginController.cs create mode 100644 src/Umbraco.Web.BackOffice/Security/DefaultBackOfficeTwoFactorOptions.cs create mode 100644 src/Umbraco.Web.BackOffice/Security/TwoFactorLoginViewOptions.cs create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/twofactorlogin.resource.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/login-2fa.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/login-2fa.html delete mode 100644 src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js delete mode 100644 src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html diff --git a/.gitignore b/.gitignore index c69474ac30..fd7951db17 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,8 @@ src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/lib/* src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/views/* src/Umbraco.Web.UI/wwwroot/Media/* src/Umbraco.Web.UI/Smidge/ +src/Umbraco.Web.UI/App_Code/ +src/Umbraco.Web.UI/App_Plugins/ # Tests cypress.env.json diff --git a/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs b/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs new file mode 100644 index 0000000000..095d4f50a9 --- /dev/null +++ b/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract] + public class UserTwoFactorProviderModel + { + public UserTwoFactorProviderModel(string providerName, bool isEnabledOnUser) + { + ProviderName = providerName; + IsEnabledOnUser = isEnabledOnUser; + } + + [DataMember(Name = "providerName")] + public string ProviderName { get; } + + [DataMember(Name = "isEnabledOnUser")] + public bool IsEnabledOnUser { get; } + } +} diff --git a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs index 33a96ad751..1855d03fe1 100644 --- a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs +++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs @@ -58,4 +58,12 @@ namespace Umbraco.Cms.Core.Services /// Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey); } + + [Obsolete("This will be merged into ITwoFactorLoginService in Umbraco 11")] + public interface ITwoFactorLoginService2 : ITwoFactorLoginService + { + Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code); + + Task ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code); + } } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 32c0500a79..330110721e 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -35,6 +35,7 @@ namespace Umbraco.Cms.Core.Security private readonly GlobalSettings _globalSettings; private readonly IUmbracoMapper _mapper; private readonly AppCaches _appCaches; + private readonly ITwoFactorLoginService _twoFactorLoginService; /// /// Initializes a new instance of the class. @@ -48,7 +49,8 @@ namespace Umbraco.Cms.Core.Security IOptions globalSettings, IUmbracoMapper mapper, BackOfficeErrorDescriber describer, - AppCaches appCaches) + AppCaches appCaches, + ITwoFactorLoginService twoFactorLoginService) : base(describer) { _scopeProvider = scopeProvider; @@ -58,11 +60,36 @@ namespace Umbraco.Cms.Core.Security _globalSettings = globalSettings.Value; _mapper = mapper; _appCaches = appCaches; + _twoFactorLoginService = twoFactorLoginService; _userService = userService; _externalLoginService = externalLoginService; } - [Obsolete("Use ctor injecting IExternalLoginWithKeyService ")] + [Obsolete("Use non obsolete ctor")] + public BackOfficeUserStore( + IScopeProvider scopeProvider, + IUserService userService, + IEntityService entityService, + IExternalLoginWithKeyService externalLoginService, + IOptions globalSettings, + IUmbracoMapper mapper, + BackOfficeErrorDescriber describer, + AppCaches appCaches) + : this( + scopeProvider, + userService, + entityService, + externalLoginService, + globalSettings, + mapper, + describer, + appCaches, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + [Obsolete("Use non obsolete ctor")] public BackOfficeUserStore( IScopeProvider scopeProvider, IUserService userService, @@ -80,11 +107,24 @@ namespace Umbraco.Cms.Core.Security globalSettings, mapper, describer, - appCaches) + appCaches, + StaticServiceProvider.Instance.GetRequiredService()) { } + /// + public override async Task GetTwoFactorEnabledAsync(BackOfficeIdentityUser user, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!int.TryParse(user.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intUserId)) + { + return await base.GetTwoFactorEnabledAsync(user, cancellationToken); + } + + return await _twoFactorLoginService.IsTwoFactorEnabledAsync(user.Key); + } + /// public override Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) { diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 1410473f6a..81b541ecc9 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -49,10 +49,10 @@ namespace Umbraco.Cms.Core.Security public override bool SupportsQueryableUsers => false; // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository /// - /// Developers will need to override this to support custom 2 factor auth + /// Both users and members supports 2FA /// /// - public override bool SupportsUserTwoFactor => false; + public override bool SupportsUserTwoFactor => true; /// public override bool SupportsUserPhoneNumber => false; // We haven't needed to support this yet, though might be necessary for 2FA diff --git a/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs index cdcc6b19e9..11b325e350 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs @@ -3,22 +3,26 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.Services { /// - public class TwoFactorLoginService : ITwoFactorLoginService + public class TwoFactorLoginService : ITwoFactorLoginService2 { private readonly ITwoFactorLoginRepository _twoFactorLoginRepository; private readonly IScopeProvider _scopeProvider; private readonly IOptions _identityOptions; private readonly IOptions _backOfficeIdentityOptions; private readonly IDictionary _twoFactorSetupGenerators; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -28,16 +32,34 @@ namespace Umbraco.Cms.Core.Services IScopeProvider scopeProvider, IEnumerable twoFactorSetupGenerators, IOptions identityOptions, - IOptions backOfficeIdentityOptions - ) + IOptions backOfficeIdentityOptions, + ILogger logger) { _twoFactorLoginRepository = twoFactorLoginRepository; _scopeProvider = scopeProvider; _identityOptions = identityOptions; _backOfficeIdentityOptions = backOfficeIdentityOptions; + _logger = logger; _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x =>x.ProviderName); } + [Obsolete("Use ctor with all params - This will be removed in v11")] + public TwoFactorLoginService( + ITwoFactorLoginRepository twoFactorLoginRepository, + IScopeProvider scopeProvider, + IEnumerable twoFactorSetupGenerators, + IOptions identityOptions, + IOptions backOfficeIdentityOptions) + : this(twoFactorLoginRepository, + scopeProvider, + twoFactorSetupGenerators, + identityOptions, + backOfficeIdentityOptions, + StaticServiceProvider.Instance.GetRequiredService>()) + { + + } + /// public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) { @@ -51,6 +73,56 @@ namespace Umbraco.Cms.Core.Services return await GetEnabledProviderNamesAsync(userOrMemberKey); } + public async Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code) + { + var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); + + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider generator)) + { + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); + } + + var isValid = generator.ValidateTwoFactorPIN(secret, code); + + if (!isValid) + { + return false; + } + + return await DisableAsync(userOrMemberKey, providerName); + } + + public async Task ValidateAndSaveAsync(string providerName, Guid userOrMemberKey, string secret, string code) + { + + try + { + var isValid = ValidateTwoFactorSetup(providerName, secret, code); + if (isValid == false) + { + return false; + } + + var twoFactorLogin = new TwoFactorLogin() + { + Confirmed = true, + Secret = secret, + UserOrMemberKey = userOrMemberKey, + ProviderName = providerName + }; + + await SaveAsync(twoFactorLogin); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not log in with the provided one-time-password"); + } + + return false; + } + private async Task> GetEnabledProviderNamesAsync(Guid userOrMemberKey) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index cdc9d2e913..43e691a6f3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -74,11 +74,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions; private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ITwoFactorLoginService _twoFactorLoginService; private readonly WebRoutingSettings _webRoutingSettings; // TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here [ActivatorUtilitiesConstructor] - public AuthenticationController( + public AuthenticationController( IBackOfficeSecurityAccessor backofficeSecurityAccessor, IBackOfficeUserManager backOfficeUserManager, IBackOfficeSignInManager signInManager, @@ -97,7 +98,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IBackOfficeExternalLoginProviders externalAuthenticationOptions, IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, IHttpContextAccessor httpContextAccessor, - IOptions webRoutingSettings) + IOptions webRoutingSettings, + ITwoFactorLoginService twoFactorLoginService) { _backofficeSecurityAccessor = backofficeSecurityAccessor; _userManager = backOfficeUserManager; @@ -118,9 +120,56 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions; _httpContextAccessor = httpContextAccessor; _webRoutingSettings = webRoutingSettings.Value; + _twoFactorLoginService = twoFactorLoginService; } - [Obsolete("Use constructor that also takes IHttpAccessor and IOptions, scheduled for removal in V11")] + [Obsolete("Use constructor that takes all params, scheduled for removal in V11")] + public AuthenticationController( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IBackOfficeUserManager backOfficeUserManager, + IBackOfficeSignInManager signInManager, + IUserService userService, + ILocalizedTextService textService, + IUmbracoMapper umbracoMapper, + IOptions globalSettings, + IOptions securitySettings, + ILogger logger, + IIpResolver ipResolver, + IOptions passwordConfiguration, + IEmailSender emailSender, + ISmsSender smsSender, + IHostingEnvironment hostingEnvironment, + LinkGenerator linkGenerator, + IBackOfficeExternalLoginProviders externalAuthenticationOptions, + IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, + IHttpContextAccessor httpContextAccessor, + IOptions webRoutingSettings) + : this( + backofficeSecurityAccessor, + backOfficeUserManager, + signInManager, + userService, + textService, + umbracoMapper, + globalSettings, + securitySettings, + logger, + ipResolver, + passwordConfiguration, + emailSender, + smsSender, + hostingEnvironment, + linkGenerator, + externalAuthenticationOptions, + backOfficeTwoFactorOptions, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService>(), + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + [Obsolete("Use constructor that takes all params, scheduled for removal in V11")] public AuthenticationController( IBackOfficeSecurityAccessor backofficeSecurityAccessor, IBackOfficeUserManager backOfficeUserManager, @@ -469,7 +518,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return NotFound(); } - var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user); + var userFactors = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key); + return new ObjectResult(userFactors); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index b74c4ea7b0..cbb17dce99 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -235,6 +235,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers "authenticationApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.PostLogin(null)) }, + { + "twoFactorLoginApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.SetupInfo(null)) + }, { "currentUserApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.PostChangePassword(null)) diff --git a/src/Umbraco.Web.BackOffice/Controllers/TwoFactorLoginController.cs b/src/Umbraco.Web.BackOffice/Controllers/TwoFactorLoginController.cs new file mode 100644 index 0000000000..41c25e3b47 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/TwoFactorLoginController.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Web.BackOffice.Controllers +{ + public class TwoFactorLoginController : UmbracoAuthorizedJsonController + { + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly ILogger _logger; + private readonly ITwoFactorLoginService2 _twoFactorLoginService; + private readonly IBackOfficeSignInManager _backOfficeSignInManager; + private readonly IBackOfficeUserManager _backOfficeUserManager; + private readonly IOptionsSnapshot _twoFactorLoginViewOptions; + + public TwoFactorLoginController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + ILogger logger, + ITwoFactorLoginService twoFactorLoginService, + IBackOfficeSignInManager backOfficeSignInManager, + IBackOfficeUserManager backOfficeUserManager, + IOptionsSnapshot twoFactorLoginViewOptions) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _logger = logger; + + if (twoFactorLoginService is not ITwoFactorLoginService2 twoFactorLoginService2) + { + throw new ArgumentException("twoFactorLoginService needs to implement ITwoFactorLoginService2 until the interfaces are merged", nameof(twoFactorLoginService)); + } + _twoFactorLoginService = twoFactorLoginService2; + _backOfficeSignInManager = backOfficeSignInManager; + _backOfficeUserManager = backOfficeUserManager; + _twoFactorLoginViewOptions = twoFactorLoginViewOptions; + } + + /// + /// Used to retrieve the 2FA providers for code submission + /// + /// + [HttpGet] + [AllowAnonymous] + public async Task>> GetEnabled2FAProvidersForCurrentUser() + { + var user = await _backOfficeSignInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + _logger.LogWarning("No verified user found, returning 404"); + return NotFound(); + } + + var userFactors = await _backOfficeUserManager.GetValidTwoFactorProvidersAsync(user); + return new ObjectResult(userFactors); + } + + + [HttpGet] + public async Task>> Get2FAProvidersForUser(int userId) + { + var user = await _backOfficeUserManager.FindByIdAsync(userId.ToString()); + + var enabledProviderNameHashSet = new HashSet(await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key)); + + var providerNames = await _backOfficeUserManager.GetValidTwoFactorProvidersAsync(user); + + return providerNames.Select(providerName => + new UserTwoFactorProviderModel(providerName, enabledProviderNameHashSet.Contains(providerName))).ToArray(); + } + + [HttpGet] + public async Task> SetupInfo(string providerName) + { + var user = _backOfficeSecurityAccessor?.BackOfficeSecurity.CurrentUser; + + var setupInfo = await _twoFactorLoginService.GetSetupInfoAsync(user.Key, providerName); + + return setupInfo; + } + + + [HttpPost] + public async Task> ValidateAndSave(string providerName, string secret, string code) + { + var user = _backOfficeSecurityAccessor?.BackOfficeSecurity.CurrentUser; + + return await _twoFactorLoginService.ValidateAndSaveAsync(providerName, user.Key, secret, code); + } + + [HttpPost] + [Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)] + public async Task> Disable(string providerName, Guid userKey) + { + return await _twoFactorLoginService.DisableAsync(userKey, providerName); + } + + [HttpPost] + public async Task> DisableWithCode(string providerName, string code) + { + Guid key = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Key; + + return await _twoFactorLoginService.DisableWithCodeAsync(providerName, key, code); + } + + [HttpGet] + public ActionResult ViewPathForProviderName(string providerName) + { + var options = _twoFactorLoginViewOptions.Get(providerName); + return options.SetupViewPath; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index 24ecee08ef..0e878aef8b 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -45,6 +45,11 @@ namespace Umbraco.Extensions { o.Cookie.Name = Constants.Security.BackOfficeTwoFactorAuthenticationType; o.ExpireTimeSpan = TimeSpan.FromMinutes(5); + }) + .AddCookie(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType, o => + { + o.Cookie.Name = Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType; + o.ExpireTimeSpan = TimeSpan.FromMinutes(5); }); builder.Services.ConfigureOptions(); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs index 1dc5bda7a9..68664d35e7 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -42,7 +42,8 @@ namespace Umbraco.Extensions factory.GetRequiredService>(), factory.GetRequiredService(), factory.GetRequiredService(), - factory.GetRequiredService() + factory.GetRequiredService(), + factory.GetRequiredService() )) .AddUserManager() .AddSignInManager() @@ -64,7 +65,7 @@ namespace Umbraco.Extensions services.TryAddScoped(); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); return new BackOfficeIdentityBuilder(services); } diff --git a/src/Umbraco.Web.BackOffice/Security/DefaultBackOfficeTwoFactorOptions.cs b/src/Umbraco.Web.BackOffice/Security/DefaultBackOfficeTwoFactorOptions.cs new file mode 100644 index 0000000000..5ef0f53695 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Security/DefaultBackOfficeTwoFactorOptions.cs @@ -0,0 +1,10 @@ +using System; + +namespace Umbraco.Cms.Web.BackOffice.Security +{ + public class DefaultBackOfficeTwoFactorOptions : IBackOfficeTwoFactorOptions + { + public string GetTwoFactorView(string username) => "views\\common\\login-2fa.html"; + } + +} diff --git a/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs b/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs index 05cc7970b4..df9bf38e79 100644 --- a/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs @@ -1,5 +1,8 @@ -namespace Umbraco.Cms.Web.BackOffice.Security +using System; + +namespace Umbraco.Cms.Web.BackOffice.Security { + [Obsolete("Not used anymore")] public class NoopBackOfficeTwoFactorOptions : IBackOfficeTwoFactorOptions { public string GetTwoFactorView(string username) => null; diff --git a/src/Umbraco.Web.BackOffice/Security/TwoFactorLoginViewOptions.cs b/src/Umbraco.Web.BackOffice/Security/TwoFactorLoginViewOptions.cs new file mode 100644 index 0000000000..da06dd9e67 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Security/TwoFactorLoginViewOptions.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Web.BackOffice.Security +{ + /// + /// Options used as named options for 2fa providers + /// + public class TwoFactorLoginViewOptions + { + /// + /// Gets or sets the path of the view to show when setting up this 2fa provider + /// + public string SetupViewPath { get; set; } + } +} diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 505abdbe0e..9d68da047c 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -74,6 +74,7 @@ namespace Umbraco.Cms.Web.Common.Security return await base.VerifyPasswordAsync(store, user, password); } + /// /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date /// diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 8124b9c50e..cb7a2b0835 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -45,9 +45,6 @@ namespace Umbraco.Cms.Web.Common.Security _httpContextAccessor = httpContextAccessor; } - /// - public override bool SupportsUserTwoFactor => true; - /// public async Task IsMemberAuthorizedAsync(IEnumerable allowTypes = null, IEnumerable allowGroups = null, IEnumerable allowMembers = null) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js index 01e199c572..dd83f6546b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function AppHeaderDirective(eventsService, appState, userService, focusService, overlayService, $timeout) { + function AppHeaderDirective(eventsService, appState, userService, focusService, $timeout, editorService) { function link(scope, element) { @@ -72,17 +72,15 @@ }; scope.avatarClick = function () { - - const dialog = { - view: "user", - position: "right", - name: "overlay-user", - close: function () { - overlayService.close(); - } + const userEditor = { + size: "small", + view: "views/common/infiniteeditors/user/user.html", + close: function() { + editorService.close(); + } }; - overlayService.open(dialog); + editorService.open(userEditor); }; scope.logoModal = { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 9986b9ce8a..d1c8d1ac85 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -443,12 +443,16 @@ vm.twoFactor.submitCallback = function submitCallback() { vm.onLogin(); } + vm.twoFactor.cancelCallback = function cancelCallback() { + vm.showLogin(); + } vm.twoFactor.view = viewPath; vm.view = "2fa-login"; SetTitle(); } function resetInputValidation() { + vm.loginStates.submitButton = "init"; vm.confirmPassword = ""; vm.password = ""; vm.login = ""; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/twofactorlogin.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/twofactorlogin.resource.js new file mode 100644 index 0000000000..8ed308812e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/twofactorlogin.resource.js @@ -0,0 +1,136 @@ +/** + * @ngdoc service + * @name umbraco.resources.twoFactorLoginResource + * @function + * + * @description + * Used by the users section to get users 2FA information + */ +(function () { + 'use strict'; + + function twoFactorLoginResource($http, umbRequestHelper) { + + /** + * @ngdoc method + * @name umbraco.resources.twoFactorLoginResource#viewPathForProviderName + * @methodOf umbraco.resources.twoFactorLoginResource + * + * @description + * Gets the view path for the specified two factor provider + * + * ##usage + *
+         * twoFactorLoginResource.viewPathForProviderName(providerName)
+         *    .then(function(viewPath) {
+         *        alert("It's here");
+         *    });
+         * 
+ * + * @returns {Promise} resourcePromise object containing the view path. + * + */ + function viewPathForProviderName(providerName) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "ViewPathForProviderName", + {providerName : providerName })), + "Failed to retrieve data"); + } + + /** + * @ngdoc method + * @name umbraco.resources.twoFactorLoginResource#get2FAProvidersForUser + * @methodOf umbraco.resources.twoFactorLoginResource + * + * @description + * Gets the 2fa provider names that is available + * + * ##usage + *
+       * twoFactorLoginResource.get2FAProvidersForUser(userKey)
+       *    .then(function(providers) {
+       *        alert("It's here");
+       *    });
+       * 
+ * + * @returns {Promise} resourcePromise object containing the an array of { providerName, isEnabledOnUser} . + * + */ + function get2FAProvidersForUser(userId) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "get2FAProvidersForUser", + { userId: userId })), + "Failed to retrieve data"); + } + + function setupInfo(providerName) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "setupInfo", + { providerName: providerName })), + "Failed to retrieve data"); + } + + function validateAndSave(providerName, secret, code) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "validateAndSave", + { + providerName: providerName, + secret: secret, + code: code + })), + "Failed to retrieve data"); + } + function disable(providerName, userKey) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "disable", + { + providerName: providerName, + userKey: userKey + })), + "Failed to retrieve data"); + } + function disableWithCode(providerName, code) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "disableWithCode", + { + providerName: providerName, + code: code + })), + "Failed to retrieve data"); + } + + var resource = { + viewPathForProviderName: viewPathForProviderName, + get2FAProvidersForUser:get2FAProvidersForUser, + setupInfo:setupInfo, + validateAndSave:validateAndSave, + disable: disable, + disableWithCode: disableWithCode + }; + + return resource; + + } + + angular.module('umbraco.resources').factory('twoFactorLoginResource', twoFactorLoginResource); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js index ad02520c5a..c9db8cf00e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js @@ -273,7 +273,7 @@ angular.module('umbraco.services') */ removeAll: function () { angularHelper.safeApply($rootScope, function() { - nArray = []; + nArray.length = 0; }); }, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.controller.js new file mode 100644 index 0000000000..382d9ef35e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.controller.js @@ -0,0 +1,79 @@ +//used for the user editor overlay +angular.module("umbraco").controller("Umbraco.Editors.ConfigureTwoFactorController", + function ($scope, + localizationService, + notificationsService, + overlayService, + twoFactorLoginResource, + editorService) { + + + let vm = this; + vm.close = close; + vm.enable = enable; + vm.disable = disable; + vm.code = ""; + vm.buttonState = "init"; + + localizationService.localize("user_configureTwoFactor").then(function (value) { + vm.title = value; + }); + + function onInit() { + vm.code = ""; + + twoFactorLoginResource.get2FAProvidersForUser($scope.model.user.id) + .then(function (providers) { + vm.providers = providers; + }); + } + + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + + function enable(providerName) { + twoFactorLoginResource.viewPathForProviderName(providerName) + .then(function (viewPath) { + var providerSettings = { + user: $scope.model.user, + providerName: providerName, + size: "small", + view: viewPath, + close: function () { + notificationsService.removeAll(); + editorService.close(); + onInit(); + } + }; + + editorService.open(providerSettings); + }).catch(onError); + } + + function disable(provider) { + + const disableTwoFactorSettings = { + provider, + user: vm.user, + size: "small", + view: "views/common/infiniteeditors/twofactor/disabletwofactor.html", + close: function () { + editorService.close(); + onInit(); + } + }; + + editorService.open(disableTwoFactorSettings); + } + + function onError(error) { + vm.buttonState = "error"; + overlayService.ysod(error); + } + + //initialize + onInit(); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.html new file mode 100644 index 0000000000..15bb70b7f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.html @@ -0,0 +1,58 @@ +
+ + + + + + + + + + + + + + + + +
+

+ + +

+ + + + +
+ + +
+
+ + + + +
+ +
+
+
+ + + + + + + + + + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.controller.js new file mode 100644 index 0000000000..b8e909e633 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.controller.js @@ -0,0 +1,53 @@ +//used for the user editor overlay +angular.module("umbraco").controller("Umbraco.Editors.DisableTwoFactorController", + function ($scope, + localizationService, + notificationsService, + overlayService, + twoFactorLoginResource) { + + let vm = this; + vm.close = close; + vm.disableWithCode = disableWithCode; + vm.code = ""; + vm.buttonState = "init"; + vm.authForm = {}; + + if (!$scope.model.provider) { + notificationsService.error("No provider specified"); + } + vm.provider = $scope.model.provider; + vm.title = vm.provider.providerName; + + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + + function disableWithCode() { + vm.authForm.token.$setValidity("token", true); + vm.buttonState = "busy"; + twoFactorLoginResource.disableWithCode(vm.provider.providerName, vm.code) + .then(onResponse) + .catch(onError); + } + + function onResponse(response) { + if (response) { + vm.buttonState = "success"; + localizationService.localize("user_2faProviderIsDisabledMsg").then(function (value) { + notificationsService.info(value); + }); + close(); + } else { + vm.buttonState = "error"; + vm.authForm.token.$setValidity("token", false); + } + } + + function onError(error) { + vm.buttonState = "error"; + overlayService.ysod(error); + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.html new file mode 100644 index 0000000000..11c2c813fd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.html @@ -0,0 +1,56 @@ +
+ + +
+ + + + + + + + + + + + + + + + +
+ + Invalid code entered + +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.controller.js new file mode 100644 index 0000000000..7f9e546709 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.controller.js @@ -0,0 +1,219 @@ +angular.module("umbraco") + .controller("Umbraco.Editors.UserController", function ($scope, $location, $timeout, + dashboardResource, userService, historyService, eventsService, + externalLoginInfoService, authResource, + currentUserResource, formHelper, localizationService, editorService, twoFactorLoginResource) { + + let vm = this; + + vm.history = historyService.getCurrent(); + vm.showPasswordFields = false; + vm.changePasswordButtonState = "init"; + vm.hasTwoFactorProviders = false; + + localizationService.localize("general_user").then(function (value) { + vm.title = value; + }); + + // Set flag if any have deny local login, in which case we must disable all password functionality + vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); + // Only include login providers that have editable options + vm.externalLoginProviders = externalLoginInfoService.getLoginProvidersWithOptions(); + + vm.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; + var evts = []; + evts.push(eventsService.on("historyService.add", function (e, args) { + vm.history = args.all; + })); + evts.push(eventsService.on("historyService.remove", function (e, args) { + vm.history = args.all; + })); + evts.push(eventsService.on("historyService.removeAll", function (e, args) { + vm.history = []; + })); + + vm.logout = function () { + + //Add event listener for when there are pending changes on an editor which means our route was not successful + var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { + //one time listener, remove the event + pendingChangeEvent(); + vm.close(); + }); + + + //perform the path change, if it is successful then the promise will resolve otherwise it will fail + vm.close(); + $location.path("/logout").search(''); + }; + + vm.gotoHistory = function (link) { + $location.path(link); + vm.close(); + }; + /* + //Manually update the remaining timeout seconds + function updateTimeout() { + $timeout(function () { + if (vm.remainingAuthSeconds > 0) { + vm.remainingAuthSeconds--; + $scope.$digest(); + //recurse + updateTimeout(); + } + + }, 1000, false); // 1 second, do NOT execute a global digest + } + */ + function updateUserInfo() { + //get the user + userService.getCurrentUser().then(function (user) { + vm.user = user; + if (vm.user) { + vm.remainingAuthSeconds = vm.user.remainingAuthSeconds; + vm.canEditProfile = _.indexOf(vm.user.allowedSections, "users") > -1; + //set the timer + //updateTimeout(); + + currentUserResource.getCurrentUserLinkedLogins().then(function (logins) { + + //reset all to be un-linked + vm.externalLoginProviders.forEach(provider => provider.linkedProviderKey = undefined); + + //set the linked logins + for (var login in logins) { + var found = _.find(vm.externalLoginProviders, function (i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = logins[login]; + } + } + }); + + //go get the config for the membership provider and add it to the model + authResource.getPasswordConfig(user.id).then(function (data) { + vm.changePasswordModel.config = data; + //ensure the hasPassword config option is set to true (the user of course has a password already assigned) + //this will ensure the oldPassword is shown so they can change it + // disable reset password functionality beacuse it does not make sense inside the backoffice + vm.changePasswordModel.config.hasPassword = true; + vm.changePasswordModel.config.disableToggle = true; + }); + + twoFactorLoginResource.get2FAProvidersForUser(vm.user.id).then(function (providers) { + vm.hasTwoFactorProviders = providers.length > 0; + }); + + } + }); + + + } + + vm.linkProvider = function (e) { + e.target.submit(); + } + + vm.unlink = function (e, loginProvider, providerKey) { + var result = confirm("Are you sure you want to unlink this account?"); + if (!result) { + e.preventDefault(); + return; + } + + authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { + updateUserInfo(); + }); + } + + //create the initial model for change password + vm.changePasswordModel = { + config: {}, + value: {} + }; + + updateUserInfo(); + + + //remove all event handlers + $scope.$on('$destroy', function () { + for (var e = 0; e < evts.length; e++) { + evts[e](); + } + + }); + + vm.changePassword = function () { + + if (formHelper.submitForm({ scope: $scope })) { + + vm.changePasswordButtonState = "busy"; + + currentUserResource.changePassword(vm.changePasswordModel.value).then(function (data) { + + //reset old data + clearPasswordFields(); + + formHelper.resetForm({ scope: $scope }); + + vm.changePasswordButtonState = "success"; + $timeout(function () { + vm.togglePasswordFields(); + }, 2000); + + }, function (err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); + formHelper.handleError(err); + + vm.changePasswordButtonState = "error"; + + }); + + } + + }; + + vm.togglePasswordFields = function () { + clearPasswordFields(); + vm.showPasswordFields = !vm.showPasswordFields; + } + + function clearPasswordFields() { + vm.changePasswordModel.value.oldPassword = ""; + vm.changePasswordModel.value.newPassword = ""; + vm.changePasswordModel.value.confirm = ""; + } + + vm.editUser = function () { + $location + .path('/users/users/user/' + vm.user.id); + vm.close(); + } + + vm.toggleConfigureTwoFactor = function () { + + const configureTwoFactorSettings = { + create: true, + user: vm.user, + isCurrentUser: true,// From this view we are always current user (used by the overlay) + size: "small", + view: "views/common/infiniteeditors/twofactor/configuretwofactor.html", + close: function () { + editorService.close(); + } + }; + + editorService.open(configureTwoFactorSettings); + } + + vm.close = function () { + if ($scope.model.close) { + $scope.model.close(); + } + } + + dashboardResource.getDashboard("user-dialog").then(function (dashboard) { + vm.dashboard = dashboard; + }); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.html new file mode 100644 index 0000000000..c67f65a7d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.html @@ -0,0 +1,145 @@ +
+ + + + + + +
+ + + + +
+ + + + + + + + +
+
+
+
+ + + + + + + +
+ +
+ +
+
+ + +
+ + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + +
+ +
+ +
+ Change password +
+ +
+ + + + + + + + + + +
+ +
+ +
+
+
{{tab.label}}
+
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.controller.js new file mode 100644 index 0000000000..6c57085c8b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.controller.js @@ -0,0 +1,38 @@ +angular.module("umbraco").controller("Umbraco.Login2faController", + function ($scope, userService, authResource) { + let vm = this; + vm.code = ""; + vm.provider = ""; + vm.providers = []; + vm.stateValidateButton = "init"; + vm.authForm = {}; + + authResource.get2FAProviders() + .then(function (data) { + vm.providers = data; + if (vm.providers.length > 0) { + vm.provider = vm.providers[0]; + } + }); + + vm.validate = function () { + vm.error = ""; + vm.stateValidateButton = "busy"; + vm.authForm.token.$setValidity('token', true); + + authResource.verify2FACode(vm.provider, vm.code) + .then(function (data) { + vm.stateValidateButton = "success"; + userService.setAuthenticationSuccessful(data); + $scope.vm.twoFactor.submitCallback(); + }) + .catch(function () { + vm.stateValidateButton = "error"; + vm.authForm.token.$setValidity('token', false); + }); + }; + + vm.goBack = function () { + $scope.vm.twoFactor.cancelCallback(); + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.html b/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.html new file mode 100644 index 0000000000..fd444f180c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.html @@ -0,0 +1,44 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js deleted file mode 100644 index a98eacd702..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js +++ /dev/null @@ -1,194 +0,0 @@ -angular.module("umbraco") - .controller("Umbraco.Overlays.UserController", function ($scope, $location, $timeout, - dashboardResource, userService, historyService, eventsService, - externalLoginInfo, externalLoginInfoService, authResource, - currentUserResource, formHelper, localizationService) { - - $scope.history = historyService.getCurrent(); - //$scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; - $scope.showPasswordFields = false; - $scope.changePasswordButtonState = "init"; - $scope.model.title = "user.name"; - //$scope.model.subtitle = "Umbraco version" + " " + $scope.version; - /* - if(!$scope.model.title) { - localizationService.localize("general_user").then(function(value){ - $scope.model.title = value; - }); - } - */ - - // Set flag if any have deny local login, in which case we must disable all password functionality - $scope.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); - // Only include login providers that have editable options - $scope.externalLoginProviders = externalLoginInfoService.getLoginProvidersWithOptions(); - - $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; - var evts = []; - evts.push(eventsService.on("historyService.add", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.remove", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.removeAll", function (e, args) { - $scope.history = []; - })); - - $scope.logout = function () { - - //Add event listener for when there are pending changes on an editor which means our route was not successful - var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { - //one time listener, remove the event - pendingChangeEvent(); - $scope.model.close(); - }); - - - //perform the path change, if it is successful then the promise will resolve otherwise it will fail - $scope.model.close(); - $location.path("/logout").search(''); - }; - - $scope.gotoHistory = function (link) { - $location.path(link); - $scope.model.close(); - }; - /* - //Manually update the remaining timeout seconds - function updateTimeout() { - $timeout(function () { - if ($scope.remainingAuthSeconds > 0) { - $scope.remainingAuthSeconds--; - $scope.$digest(); - //recurse - updateTimeout(); - } - - }, 1000, false); // 1 second, do NOT execute a global digest - } - */ - function updateUserInfo() { - //get the user - userService.getCurrentUser().then(function (user) { - $scope.user = user; - if ($scope.user) { - $scope.model.title = user.name; - $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; - $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; - //set the timer - //updateTimeout(); - - currentUserResource.getCurrentUserLinkedLogins().then(function(logins) { - - //reset all to be un-linked - $scope.externalLoginProviders.forEach(provider => provider.linkedProviderKey = undefined); - - //set the linked logins - for (var login in logins) { - var found = _.find($scope.externalLoginProviders, function (i) { - return i.authType == login; - }); - if (found) { - found.linkedProviderKey = logins[login]; - } - } - }); - - //go get the config for the membership provider and add it to the model - authResource.getPasswordConfig(user.id).then(function (data) { - $scope.changePasswordModel.config = data; - //ensure the hasPassword config option is set to true (the user of course has a password already assigned) - //this will ensure the oldPassword is shown so they can change it - // disable reset password functionality beacuse it does not make sense inside the backoffice - $scope.changePasswordModel.config.hasPassword = true; - $scope.changePasswordModel.config.disableToggle = true; - }); - - } - }); - } - - $scope.linkProvider = function (e) { - e.target.submit(); - } - - $scope.unlink = function (e, loginProvider, providerKey) { - var result = confirm("Are you sure you want to unlink this account?"); - if (!result) { - e.preventDefault(); - return; - } - - authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { - updateUserInfo(); - }); - } - - //create the initial model for change password - $scope.changePasswordModel = { - config: {}, - value: {} - }; - - updateUserInfo(); - - //remove all event handlers - $scope.$on('$destroy', function () { - for (var e = 0; e < evts.length; e++) { - evts[e](); - } - - }); - - $scope.changePassword = function() { - - if (formHelper.submitForm({ scope: $scope })) { - - $scope.changePasswordButtonState = "busy"; - - currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) { - - //reset old data - clearPasswordFields(); - - formHelper.resetForm({ scope: $scope }); - - $scope.changePasswordButtonState = "success"; - $timeout(function() { - $scope.togglePasswordFields(); - }, 2000); - - }, function (err) { - formHelper.resetForm({ scope: $scope, hasErrors: true }); - formHelper.handleError(err); - - $scope.changePasswordButtonState = "error"; - - }); - - } - - }; - - $scope.togglePasswordFields = function() { - clearPasswordFields(); - $scope.showPasswordFields = !$scope.showPasswordFields; - } - - function clearPasswordFields() { - $scope.changePasswordModel.value.oldPassword = ""; - $scope.changePasswordModel.value.newPassword = ""; - $scope.changePasswordModel.value.confirm = ""; - } - - $scope.editUser = function() { - $location - .path('/users/users/user/' + $scope.user.id); - $scope.model.close(); - } - - dashboardResource.getDashboard("user-dialog").then(function (dashboard) { - $scope.dashboard = dashboard; - }); - }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html deleted file mode 100644 index 24acef995e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ /dev/null @@ -1,143 +0,0 @@ -
-
- -
- Your profile -
- - - - - - - - - - -
- -
- -
- External login providers -
- -
- -
- -
-
- - -
- - -
- -
- -
- - -
-
- Your recent history -
- -
- -
- -
- Change password -
- -
- - - - - - - - - - -
- -
- -
-
-
{{tab.label}}
-
-
-
-
-
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 684ce6d2f0..f9d48d95e9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserEditController($scope, eventsService, $q, $location, $routeParams, formHelper, usersResource, + function UserEditController($scope, eventsService, $q, $location, $routeParams, formHelper, usersResource, twoFactorLoginResource, userService, contentEditingHelper, localizationService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper, editorService, overlayService, externalLoginInfoService) { @@ -16,6 +16,7 @@ }; vm.breadcrumbs = []; vm.showBackButton = true; + vm.hasTwoFactorProviders = false; vm.avatarFile = {}; vm.labels = {}; vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; @@ -38,6 +39,7 @@ vm.disableUser = disableUser; vm.enableUser = enableUser; vm.unlockUser = unlockUser; + vm.toggleConfigureTwoFactor = toggleConfigureTwoFactor; vm.resendInvite = resendInvite; vm.deleteNonLoggedInUser = deleteNonLoggedInUser; vm.changeAvatar = changeAvatar; @@ -101,6 +103,10 @@ $scope.$emit("$setAccessibleHeader", false, "general_user", false, vm.user.name, "", true); vm.loading = false; }); + + twoFactorLoginResource.get2FAProvidersForUser(vm.user.id).then(function (providers) { + vm.hasTwoFactorProviders = providers.length > 0; + }); }); } @@ -397,6 +403,23 @@ }); } + function toggleConfigureTwoFactor() { + + var configureTwoFactorSettings = { + create: true, + user: vm.user, + isCurrentUser: vm.user.isCurrentUser, + size: "small", + view: "views/common/infiniteeditors/twofactor/configuretwofactor.html", + close: function() { + editorService.close(); + } + }; + + editorService.open(configureTwoFactorSettings); + } + + function resendInvite() { vm.resendInviteButtonState = "busy"; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.controller.js index f1df6c3228..67d1efec85 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.controller.js @@ -6,6 +6,7 @@ var vm = this; vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); + } angular.module("umbraco").controller("Umbraco.Editors.Users.DetailsController", DetailsController); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index 5a4181c9f3..eaa92b7a6e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -274,6 +274,16 @@ size="s"> +
+ + +
+
+ + <_ContentIncludedByDefault Remove="wwwroot\umbraco\views\common\infiniteeditors\twofactor\enabletwofactor.html" /> + + false false diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 5ec919874f..9916973405 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -565,11 +565,11 @@ Du tilføjer flere sprog under 'sprog' i menuen til venstre - ]]> + ]]> Kulturnavn + ]]> Ordbogsoversigt @@ -902,7 +902,7 @@ Database konfiguration installér knappen for at installere Umbraco %0% databasen - ]]> + ]]> Næste for at fortsætte.]]> Databasen er ikke fundet. Kontrollér venligst at informationen i database forbindelsesstrengen i "web.config" filen er korrekt.

@@ -981,7 +981,7 @@ /web.config filen og opdatére 'AppSetting' feltet UmbracoConfigurationStatus i bunden til '%0%'.]]> komme igang med det samme ved at klikke på "Start Umbraco" knappen nedenfor.
Hvis du er ny med Umbraco, kan du finde masser af ressourcer på vores 'getting started' sider. -]]>
+]]>
Start UmbracoFor at administrere dit website skal du blot åbne Umbraco administrationen og begynde at tilføje indhold, opdatere skabelonerne og stylesheets'ene eller tilføje ny funktionalitet.]]> Forbindelse til databasen fejlede. @@ -1028,6 +1028,12 @@ Umbraco: Nulstil adgangskode Dit brugernavn til at logge på Umbraco backoffice er: %0%

Klik her for at nulstille din adgangskode eller kopier/indsæt denne URL i din browser:

%1%

]]>
+ Sidste skridt + Det er påkrævet at du verificerer din identitet. + Vælg venligst en autentificeringsmetode + Kode + Indtast venligst koden fra dit device + Koden kunne ikke genkendes Skrivebord @@ -1065,7 +1071,7 @@ Gå til http://%4%/#/content/content/edit/%5% for at redigere. Ha' en dejlig dag! Mange hilsner fra Umbraco robotten - ]]> + ]]> Hej %0%

Dette er en automatisk mail for at informere dig om at opgaven '%1%' er blevet udførtpå siden '%2%' af brugeren '%3%'

@@ -1166,14 +1172,14 @@ Mange hilsner fra Umbraco robotten Udgivelsen kunne ikke udgives da publiceringsdato er sat + ]]>
+ ]]> + ]]> %0% kunne ikke udgives, fordi et 3. parts modul annullerede handlingen @@ -1453,24 +1459,24 @@ Mange hilsner fra Umbraco robotten @RenderBody() element. - ]]> + ]]> Definer en sektion @section { ... }. Herefter kan denne sektion flettes ind i overliggende skabelon ved at indsætte et @RenderSection element. - ]]> + ]]> Indsæt en sektion @RenderSection(name) element. Den underliggende skabelon skal have defineret en sektion via et @section [name]{ ... } element. - ]]> + ]]> Sektionsnavn Sektionen er obligatorisk @section -definition. - ]]> + ]]> Query builder sider returneret, på Returner @@ -1918,6 +1924,9 @@ Mange hilsner fra Umbraco robotten Ældste Sidst logget ind Ingen brugere er blevet tilføjet + Hvis du ønsker at slå denne autentificeringsmetode fra, så skal du nu indtaste koden fra dit device: + Denne autentificeringsmetode er slået til + Den valgte autentificeringsmetode er nu slået fra Validering diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 23277ee296..6670f692b8 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1149,6 +1149,12 @@ To manage your website, simply open the Umbraco backoffice and start adding cont ]]> Umbraco: Security Code Your security code is: %0% + One last step + You have enabled 2-factor authentication and must verify your identity. + Please choose a 2-factor provider + Verification code + Please enter the verification code + Invalid code entered Dashboard @@ -2219,6 +2225,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Oldest Last login No user groups have been added + If you wish to disable this two-factor provider, then you must enter the code shown on your authentication device: + This two-factor provider is enabled + This two-factor provider is now disabled Validation diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 5e2935b395..c901b1040a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -587,11 +587,11 @@ %0%' below - ]]> + ]]> Culture Name + ]]> Dictionary overview @@ -862,6 +862,7 @@ URL User Username + Validate Value View Welcome... @@ -933,7 +934,7 @@ Database configuration install button to install the Umbraco %0% database - ]]> + ]]> Next to proceed.]]> Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

@@ -951,7 +952,7 @@

Don't worry - no content will be deleted and everything will continue working afterwards!

- ]]>
+ ]]> Press Next to proceed. ]]> @@ -997,19 +998,19 @@ + ]]> I want to start from scratch learn how) You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> + ]]> You've just set up a clean Umbraco platform. What do you want to do next? Runway is installed This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> + ]]> Only recommended for experienced users I want to start with a simple website Included with Runway: Home page, Getting Started page, Installing Modules page.
Optional Modules: Top Navigation, Sitemap, Contact, Gallery. - ]]>
+ ]]> What is Runway Step 1/5 Accept license Step 2/5: Database configuration @@ -1100,72 +1101,78 @@ To manage your website, simply open the Umbraco backoffice and start adding cont -
- - - - - - - - - - - - + +
-
-
- - - + +
- - - + + +
-

+
+ +

+
+ + + + + + +
+
+

+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ +%1% + +
+

+
+ + + +


+
+ + + + + + + ]]> + One last step + You have enabled 2-factor authentication and must verify your identity. + Please choose a 2-factor provider + Verification code + Please enter the verification code + Invalid code entered Dashboard @@ -1205,7 +1212,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Have a nice day! Cheers from the Umbraco robot - ]]> + ]]> The following languages have been modified %0% @@ -1222,70 +1229,70 @@ To manage your website, simply open the Umbraco backoffice and start adding cont -
- - - - - - - - - - - - + +
-
-
- - - + +
- - - + + +
-

+
+ +

+
+ + + + - - -
+
+
+ + + - -
+ + + - -
+

Hi %0%,

-

+

This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' -

- - - - - - -
- -
- EDIT
-
-

-

Update summary:

+

+ + + + + + +
+ +
+EDIT
+
+

+

Update summary:

%6%

-

+

Have a nice day!

Cheers from the Umbraco robot

-
-
-


-
-
- - - ]]> +
+ + + +


+ + + + + + + + ]]>
The following languages have been modified:

%0% - ]]>
+ ]]> [%0%] Notification about %1% performed on %2% Notifications @@ -1296,7 +1303,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. - ]]> + ]]> This will delete the package Include all child nodes Installed @@ -1388,22 +1395,22 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Insufficient user permissions to publish all descendant documents + ]]> + ]]> + ]]> + ]]> + ]]> + ]]> Validation failed for required language '%0%'. This @@ -1416,7 +1423,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Publish %0% and all its subpages Publish to publish %0% and thereby making its content publicly available.

You can publish this page and all its subpages by checking Include unpublished subpages below. - ]]>
+ ]]>
You have not configured any approved colors @@ -1694,23 +1701,23 @@ To manage your website, simply open the Umbraco backoffice and start adding cont @RenderBody() placeholder. - ]]> + ]]> Define a named section @section { ... }. This can be rendered in a specific area of the parent of this template, by using @RenderSection. - ]]> + ]]> Render a named section @RenderSection(name) placeholder. This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. - ]]> + ]]> Section Name Section is mandatory @section definition, otherwise an error is shown. - ]]> + ]]> Query builder items returned, in I want @@ -2002,7 +2009,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Have a nice day! Cheers from the Umbraco robot - ]]> + ]]> No translator users found. Please create a translator user before you start sending content to translation @@ -2178,6 +2185,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont you. Click the circle above to upload your photo. Writer + Configure Two-Factor Change Your profile Your recent history @@ -2202,82 +2210,82 @@ To manage your website, simply open the Umbraco backoffice and start adding cont -
- - - - - - - - - - - - + +
-
-
- - - + +
- - - + + +
-

+
+ +

+
+ + + + + + +
+
+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ +%3% + +
+

+
+ + + +


+ + + + + + +]]> Resending invitation... Delete User Are you sure you wish to delete this user account? @@ -2293,6 +2301,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Oldest Last login No user groups have been added + If you wish to disable this two-factor provider, then you must enter the code shown on your authentication device: + This two-factor provider is enabled + This two-factor provider is now disabled Validation diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts index dc1232e148..dbb82f53f4 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts @@ -93,7 +93,7 @@ function runBackOfficeIntroTour(percentageComplete, buttonText, timeout) { cy.get('.umb-tour-step__counter', { timeout: timeout }).contains('9/13'); cy.get('.umb-tour-step__footer .umb-button').should('be.visible').click(); cy.get('.umb-tour-step__counter', { timeout: timeout }).contains('10/13'); - cy.get('.umb-overlay-drawer__align-right .umb-button').should('be.visible').click(); + cy.get('[data-element~="overlay-user"] [data-element="button-overlayClose"]').should('be.visible').click(); cy.get('.umb-tour-step__counter', { timeout: timeout }).contains('11/13'); cy.umbracoGlobalHelp().click() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs index ac0d19040e..d794af09e4 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs @@ -26,6 +26,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security private IExternalLoginWithKeyService ExternalLoginService => GetRequiredService(); private IUmbracoMapper UmbracoMapper => GetRequiredService(); private ILocalizedTextService TextService => GetRequiredService(); + private ITwoFactorLoginService TwoFactorLoginService => GetRequiredService(); private BackOfficeUserStore GetUserStore() => new BackOfficeUserStore( @@ -36,7 +37,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security Options.Create(GlobalSettings), UmbracoMapper, new BackOfficeErrorDescriber(TextService), - AppCaches); + AppCaches, + TwoFactorLoginService + ); [Test] public async Task Can_Persist_Is_Approved() From c7c3a68691b3f2a5bf38d221db8ea0680133b109 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 19 Apr 2022 08:55:13 +0200 Subject: [PATCH 4/4] fixes breaking changes by reintroducing old ctor --- .../Trees/MemberGroupTreeController.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs index 858a1c8184..e41f865981 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs @@ -1,14 +1,17 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.DependencyInjection; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Trees @@ -21,6 +24,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees { private readonly IMemberGroupService _memberGroupService; + [ActivatorUtilitiesConstructor] public MemberGroupTreeController( ILocalizedTextService localizedTextService, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, @@ -31,6 +35,24 @@ namespace Umbraco.Cms.Web.BackOffice.Trees : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, eventAggregator, memberTypeService) => _memberGroupService = memberGroupService; + [Obsolete("Use ctor with all params")] + public MemberGroupTreeController( + ILocalizedTextService localizedTextService, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + IMenuItemCollectionFactory menuItemCollectionFactory, + IMemberGroupService memberGroupService, + IEventAggregator eventAggregator) + : this(localizedTextService, + umbracoApiControllerTypeCollection, + menuItemCollectionFactory, + memberGroupService, + eventAggregator, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + protected override IEnumerable GetTreeNodesFromService(string id, FormCollection queryStrings) => _memberGroupService.GetAll() .OrderBy(x => x.Name)