diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 7d4dd45fb8..982ba8c63e 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -11,6 +11,8 @@ namespace Umbraco.Cms.Core.Configuration.Models [UmbracoOptions(Constants.Configuration.ConfigSecurity)] public class SecuritySettings { + internal const bool StaticMemberBypassTwoFactorForExternalLogins = true; + internal const bool StaticUserBypassTwoFactorForExternalLogins = true; internal const bool StaticKeepUserLoggedIn = false; internal const bool StaticHideDisabledUsersInBackOffice = false; internal const bool StaticAllowPasswordReset = true; @@ -66,5 +68,17 @@ namespace Umbraco.Cms.Core.Configuration.Models /// Gets or sets a value for the member password settings. /// public MemberPasswordConfigurationSettings MemberPassword { get; set; } + + /// + /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login for members. Thereby rely on the External login and potential 2FA at that provider. + /// + [DefaultValue(StaticMemberBypassTwoFactorForExternalLogins)] + public bool MemberBypassTwoFactorForExternalLogins { get; set; } = StaticMemberBypassTwoFactorForExternalLogins; + + /// + /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login for users. Thereby rely on the External login and potential 2FA at that provider. + /// + [DefaultValue(StaticUserBypassTwoFactorForExternalLogins)] + public bool UserBypassTwoFactorForExternalLogins { get; set; } = StaticUserBypassTwoFactorForExternalLogins; } } diff --git a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs index dd11f864fb..33a96ad751 100644 --- a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs +++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs @@ -5,24 +5,57 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Services { + /// + /// Service handling 2FA logins. + /// public interface ITwoFactorLoginService : IService { /// - /// Deletes all user logins - normally used when a member is deleted + /// Deletes all user logins - normally used when a member is deleted. /// Task DeleteUserLoginsAsync(Guid userOrMemberKey); - Task IsTwoFactorEnabledAsync(Guid userKey); - Task GetSecretForUserAndProviderAsync(Guid userKey, string providerName); + /// + /// Checks whether 2FA is enabled for the user or member with the specified key. + /// + Task IsTwoFactorEnabledAsync(Guid userOrMemberKey); + /// + /// Gets the secret for user or member and a specific provider. + /// + Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName); + + /// + /// Gets the setup info for a specific user or member and a specific provider. + /// + /// + /// The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by the provider. + /// Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName); + /// + /// Gets all registered providers names. + /// IEnumerable GetAllProviderNames(); + + /// + /// Disables the 2FA provider with the specified provider name for the specified user or member. + /// Task DisableAsync(Guid userOrMemberKey, string providerName); + /// + /// Validates the setup of the provider using the secret and code. + /// bool ValidateTwoFactorSetup(string providerName, string secret, string code); + + /// + /// Saves the 2FA login information. + /// Task SaveAsync(TwoFactorLogin twoFactorLogin); + + /// + /// Gets all the enabled 2FA providers for the user or member with the specified key. + /// Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey); } - } diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 4fba880e81..420d66b0b4 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -112,6 +112,9 @@ namespace Umbraco.Cms.Core.Security // create the member _memberService.Save(memberEntity); + //We need to add roles now that the member has an Id. It do not work implicit in UpdateMemberProperties + _memberService.AssignRoles(new[] { memberEntity.Id }, user.Roles.Select(x => x.RoleId).ToArray()); + if (!memberEntity.HasIdentity) { throw new DataException("Could not create the member, check logs for details"); diff --git a/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs index 713a73c1df..cdcc6b19e9 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs @@ -11,31 +11,41 @@ using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Core.Services { + /// public class TwoFactorLoginService : ITwoFactorLoginService { private readonly ITwoFactorLoginRepository _twoFactorLoginRepository; private readonly IScopeProvider _scopeProvider; private readonly IOptions _identityOptions; + private readonly IOptions _backOfficeIdentityOptions; private readonly IDictionary _twoFactorSetupGenerators; + /// + /// Initializes a new instance of the class. + /// public TwoFactorLoginService( ITwoFactorLoginRepository twoFactorLoginRepository, IScopeProvider scopeProvider, IEnumerable twoFactorSetupGenerators, - IOptions identityOptions) + IOptions identityOptions, + IOptions backOfficeIdentityOptions + ) { _twoFactorLoginRepository = twoFactorLoginRepository; _scopeProvider = scopeProvider; _identityOptions = identityOptions; - _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x=>x.ProviderName); + _backOfficeIdentityOptions = backOfficeIdentityOptions; + _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x =>x.ProviderName); } + /// public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey); } + /// public async Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey) { return await GetEnabledProviderNamesAsync(userOrMemberKey); @@ -47,26 +57,46 @@ namespace Umbraco.Cms.Core.Services var providersOnUser = (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)) .Select(x => x.ProviderName).ToArray(); - return providersOnUser.Where(x => _identityOptions.Value.Tokens.ProviderMap.ContainsKey(x)); + return providersOnUser.Where(IsKnownProviderName); } + /// + /// The provider needs to be registered as either a member provider or backoffice provider to show up. + /// + private bool IsKnownProviderName(string providerName) + { + if (_identityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) + { + return true; + } + if (_backOfficeIdentityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) + { + return true; + } + + return false; + } + + /// public async Task IsTwoFactorEnabledAsync(Guid userOrMemberKey) { return (await GetEnabledProviderNamesAsync(userOrMemberKey)).Any(); } + /// public async Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); - return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)).FirstOrDefault(x=>x.ProviderName == providerName)?.Secret; + return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)).FirstOrDefault(x => x.ProviderName == providerName)?.Secret; } + /// public async Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName) { var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); - //Dont allow to generate a new secrets if user already has one + // Dont allow to generate a new secrets if user already has one if (!string.IsNullOrEmpty(secret)) { return default; @@ -82,14 +112,17 @@ namespace Umbraco.Cms.Core.Services return await generator.GetSetupDataAsync(userOrMemberKey, secret); } + /// public IEnumerable GetAllProviderNames() => _twoFactorSetupGenerators.Keys; + + /// public async Task DisableAsync(Guid userOrMemberKey, string providerName) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); - return (await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName)); - + return await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName); } + /// public bool ValidateTwoFactorSetup(string providerName, string secret, string code) { if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider generator)) @@ -100,6 +133,7 @@ namespace Umbraco.Cms.Core.Services return generator.ValidateTwoFactorSetup(secret, code); } + /// public Task SaveAsync(TwoFactorLogin twoFactorLogin) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); @@ -108,7 +142,6 @@ namespace Umbraco.Cms.Core.Services return Task.CompletedTask; } - /// /// Generates a new random unique secret. /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index aa5fc83e1e..e866409c17 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -31,6 +32,7 @@ using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -68,7 +70,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions; private readonly IManifestParser _manifestParser; private readonly ServerVariablesParser _serverVariables; + private readonly IOptions _securitySettings; + + [ActivatorUtilitiesConstructor] public BackOfficeController( IBackOfficeUserManager userManager, IRuntimeState runtimeState, @@ -87,7 +92,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IHttpContextAccessor httpContextAccessor, IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, IManifestParser manifestParser, - ServerVariablesParser serverVariables) + ServerVariablesParser serverVariables, + IOptions securitySettings) { _userManager = userManager; _runtimeState = runtimeState; @@ -107,6 +113,51 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions; _manifestParser = manifestParser; _serverVariables = serverVariables; + _securitySettings = securitySettings; + } + + [Obsolete("Use ctor with all params. This overload will be removed in Umbraco 10.")] + public BackOfficeController( + IBackOfficeUserManager userManager, + IRuntimeState runtimeState, + IRuntimeMinifier runtimeMinifier, + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + ILocalizedTextService textService, + IGridConfig gridConfig, + BackOfficeServerVariables backOfficeServerVariables, + AppCaches appCaches, + IBackOfficeSignInManager signInManager, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + ILogger logger, + IJsonSerializer jsonSerializer, + IBackOfficeExternalLoginProviders externalLogins, + IHttpContextAccessor httpContextAccessor, + IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, + IManifestParser manifestParser, + ServerVariablesParser serverVariables) + : this(userManager, + runtimeState, + runtimeMinifier, + globalSettings, + hostingEnvironment, + textService, + gridConfig, + backOfficeServerVariables, + appCaches, + signInManager, + backofficeSecurityAccessor, + logger, + jsonSerializer, + externalLogins, + httpContextAccessor, + backOfficeTwoFactorOptions, + manifestParser, + serverVariables, + StaticServiceProvider.Instance.GetRequiredService>() + ) + { + } [HttpGet] @@ -458,7 +509,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (response == null) throw new ArgumentNullException(nameof(response)); // Sign in the user with this external login provider (which auto links, etc...) - SignInResult result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false); + SignInResult result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false, bypassTwoFactor: _securitySettings.Value.UserBypassTwoFactorForExternalLogins); var errors = new List(); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs index e9cc213598..1dc5bda7a9 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Security; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Security; @@ -77,5 +78,14 @@ namespace Umbraco.Extensions return umbracoBuilder; } + public static BackOfficeIdentityBuilder AddTwoFactorProvider(this BackOfficeIdentityBuilder identityBuilder, string providerName) where T : class, ITwoFactorProvider + { + identityBuilder.Services.AddSingleton(); + identityBuilder.Services.AddSingleton(); + identityBuilder.AddTokenProvider>(providerName); + + return identityBuilder; + } + } } diff --git a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index 7c5a89fa71..688414b3de 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -1,11 +1,16 @@ using System; using System.Linq; +using System.Net; using System.Threading; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; using StackExchange.Profiling; +using StackExchange.Profiling.Internal; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Profiler @@ -13,6 +18,8 @@ namespace Umbraco.Cms.Web.Common.Profiler public class WebProfiler : IProfiler { + private const string WebProfileCookieKey = "umbracoWebProfiler"; + public static readonly AsyncLocal MiniProfilerContext = new AsyncLocal(x => { _ = x; @@ -39,7 +46,6 @@ namespace Umbraco.Cms.Web.Common.Profiler public void Stop(bool discardResults = false) => MiniProfilerContext.Value?.Stop(discardResults); - public void UmbracoApplicationBeginRequest(HttpContext context, RuntimeLevel runtimeLevel) { if (runtimeLevel != RuntimeLevel.Run) @@ -50,9 +56,13 @@ namespace Umbraco.Cms.Web.Common.Profiler if (ShouldProfile(context.Request)) { Start(); + ICookieManager cookieManager = GetCookieManager(context); + cookieManager.ExpireCookie(WebProfileCookieKey); //Ensure we expire the cookie, so we do not reuse the old potential value saved } } + private static ICookieManager GetCookieManager(HttpContext context) => context.RequestServices.GetRequiredService(); + public void UmbracoApplicationEndRequest(HttpContext context, RuntimeLevel runtimeLevel) { if (runtimeLevel != RuntimeLevel.Run) @@ -70,19 +80,42 @@ namespace Umbraco.Cms.Web.Common.Profiler var first = Interlocked.Exchange(ref _first, 1) == 0; if (first) { - - var startupDuration = _startupProfiler.Root.DurationMilliseconds.GetValueOrDefault(); - MiniProfilerContext.Value.DurationMilliseconds += startupDuration; - MiniProfilerContext.Value.GetTimingHierarchy().First().DurationMilliseconds += startupDuration; - MiniProfilerContext.Value.Root.AddChild(_startupProfiler.Root); + AddSubProfiler(_startupProfiler); _startupProfiler = null; } + + ICookieManager cookieManager = GetCookieManager(context); + var cookieValue = cookieManager.GetCookieValue(WebProfileCookieKey); + + if (cookieValue is not null) + { + AddSubProfiler(MiniProfiler.FromJson(cookieValue)); + } + + //If it is a redirect to a relative path (local redirect) + if (context.Response.StatusCode == (int)HttpStatusCode.Redirect + && context.Response.Headers.TryGetValue(Microsoft.Net.Http.Headers.HeaderNames.Location, out var location) + && !location.Contains("://")) + { + MiniProfilerContext.Value.Root.Name = "Before Redirect"; + cookieManager.SetCookieValue(WebProfileCookieKey, MiniProfilerContext.Value.ToJson()); + } + } } } + private void AddSubProfiler(MiniProfiler subProfiler) + { + var startupDuration = subProfiler.Root.DurationMilliseconds.GetValueOrDefault(); + MiniProfilerContext.Value.DurationMilliseconds += startupDuration; + MiniProfilerContext.Value.GetTimingHierarchy().First().DurationMilliseconds += startupDuration; + MiniProfilerContext.Value.Root.AddChild(subProfiler.Root); + + } + private static bool ShouldProfile(HttpRequest request) { if (request.IsClientSideRequest()) return false; diff --git a/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs b/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs index 32b3226440..d4272515e5 100644 --- a/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs +++ b/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs @@ -15,7 +15,7 @@ namespace Umbraco.Cms.Infrastructure.Security public class TwoFactorBackOfficeValidationProvider : TwoFactorValidationProvider where TTwoFactorSetupGenerator : ITwoFactorProvider { - protected TwoFactorBackOfficeValidationProvider(IDataProtectionProvider dataProtectionProvider, IOptions options, ILogger> logger, ITwoFactorLoginService twoFactorLoginService, TTwoFactorSetupGenerator generator) : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator) + public TwoFactorBackOfficeValidationProvider(IDataProtectionProvider dataProtectionProvider, IOptions options, ILogger> logger, ITwoFactorLoginService twoFactorLoginService, TTwoFactorSetupGenerator generator) : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator) { } diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_large_blue.svg b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_large_blue.svg new file mode 100644 index 0000000000..ce15dd3092 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_large_blue.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg index b27ae89e91..c0bdbdd40c 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg +++ b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg @@ -1,3 +1,41 @@ - - - + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg new file mode 100644 index 0000000000..b27ae89e91 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg @@ -0,0 +1,3 @@ + + + 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 b52b0a5763..6cf6dd85f3 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,9 +1,9 @@ (function () { "use strict"; - function AppHeaderDirective(eventsService, appState, userService, focusService, backdropService, overlayService) { + function AppHeaderDirective(eventsService, appState, userService, focusService, overlayService, $timeout) { - function link(scope, el, attr, ctrl) { + function link(scope, element) { var evts = []; @@ -84,6 +84,35 @@ overlayService.open(dialog); }; + scope.logoModal = { + show: false, + text: "", + timer: null + }; + scope.showLogoModal = function() { + $timeout.cancel(scope.logoModal.timer); + scope.logoModal.show = true; + scope.logoModal.text = "version "+Umbraco.Sys.ServerVariables.application.version; + $timeout(function () { + const anchorLink = element[0].querySelector('.umb-app-header__logo-modal'); + if(anchorLink) { + anchorLink.focus(); + } + }); + }; + scope.keepLogoModal = function() { + $timeout.cancel(scope.logoModal.timer); + }; + scope.hideLogoModal = function() { + $timeout.cancel(scope.logoModal.timer); + scope.logoModal.timer = $timeout(function () { + scope.logoModal.show = false; + }, 100); + }; + scope.stopClickEvent = function($event) { + $event.stopPropagation(); + }; + } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 22baed8472..09c1659775 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -101,11 +101,30 @@ /** * Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName. - * @param {Object} blockObject BlockObject to recive data values from. + * @param {Object} blockObject BlockObject to receive data values from. */ function getBlockLabel(blockObject) { if (blockObject.labelInterpolator !== undefined) { - var labelVars = Object.assign({"$contentTypeName": blockObject.content.contentTypeName, "$settings": blockObject.settingsData || {}, "$layout": blockObject.layout || {}, "$index": (blockObject.index || 0)+1 }, blockObject.data); + // blockobject.content may be null if the block is no longer allowed, + // so try and fall back to the label in the config, + // if that too is null, there's not much we can do, so just default to empty string. + var contentTypeName; + if(blockObject.content != null){ + contentTypeName = blockObject.content.contentTypeName; + } + else if(blockObject.config != null && blockObject.config.label != null){ + contentTypeName = blockObject.config.label; + } + else { + contentTypeName = ""; + } + + var labelVars = Object.assign({ + "$contentTypeName": contentTypeName, + "$settings": blockObject.settingsData || {}, + "$layout": blockObject.layout || {}, + "$index": (blockObject.index || 0)+1 + }, blockObject.data); var label = blockObject.labelInterpolator(labelVars); if (label) { return label; @@ -511,10 +530,10 @@ * @methodOf umbraco.services.blockEditorModelObject * @description Retrieve a Block Object for the given layout entry. * The Block Object offers the necessary data to display and edit a block. - * The Block Object setups live syncronization of content and settings models back to the data of your Property Editor model. - * The returned object, named ´BlockObject´, contains several usefull models to make editing of this block happen. + * The Block Object setups live synchronization of content and settings models back to the data of your Property Editor model. + * The returned object, named ´BlockObject´, contains several useful models to make editing of this block happen. * The ´BlockObject´ contains the following properties: - * - key {string}: runtime generated key, usefull for tracking of this object + * - key {string}: runtime generated key, useful for tracking of this object * - content {Object}: Content model, the content data in a ElementType model. * - settings {Object}: Settings model, the settings data in a ElementType model. * - config {Object}: A local deep copy of the block configuration model. @@ -522,12 +541,11 @@ * - updateLabel {Method}: Method to trigger an update of the label for this block. * - data {Object}: A reference to the content data object from your property editor model. * - settingsData {Object}: A reference to the settings data object from your property editor model. - * - layout {Object}: A refernce to the layout entry from your property editor model. + * - layout {Object}: A reference to the layout entry from your property editor model. * @param {Object} layoutEntry the layout entry object to build the block model from. - * @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasnt found for this block. + * @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasn't found for this block. */ getBlockObject: function (layoutEntry) { - var contentUdi = layoutEntry.contentUdi; var dataModel = getDataByUdi(contentUdi, this.value.contentData); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less index 68a29df89e..bb346fc402 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less @@ -2,12 +2,65 @@ background: @blueExtraDark; display: flex; align-items: center; - justify-content: space-between; max-width: 100%; height: @appHeaderHeight; padding: 0 20px; } +.umb-app-header__logo { + margin-right: 30px; + button { + img { + height: 30px; + } + } +} + +.umb-app-header__logo-modal { + position: absolute; + z-index: @zindexUmbOverlay; + top: 50px; + left: 17px; + font-size: 13px; + + border-radius: 6px; + + width: 160px; + padding: 20px 20px; + background-color:@white; + color: @blueExtraDark; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .14), 0 1px 6px 1px rgba(0, 0, 0, .14); + text-decoration: none; + + text-align: center; + + &::before { + content:''; + position: absolute; + transform: rotate(45deg); + background-color:@white; + top: -4px; + left: 14px; + width: 8px; + height: 8px; + } + + img { + display: block; + height: auto; + width: 120px; + margin-left: auto; + margin-right: auto; + margin-bottom: 3px; + } +} + +.umb-app-header__right { + display: flex; + align-items: center; + margin-left: auto; +} + .umb-app-header__actions { display: flex; list-style: none; diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index 2763a879ea..015c291564 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -28,14 +28,14 @@ .login-overlay__logo { position: absolute; - top: 22px; - left: 25px; + top: 12.5px; + left: 20px; + right: 25px; height: 30px; z-index: 1; } - -.login-overlay__logo > img { - max-height:100%; +.login-overlay__logo img { + height: 100%; } .login-overlay .umb-modalcolumn { @@ -69,7 +69,8 @@ margin-right: 25px; margin-top: auto; margin-bottom: auto; - border-radius: @baseBorderRadius; + border-radius: @doubleBorderRadius; + box-shadow: 0 1px 6px 1px rgba(0, 0, 0, 0.12); } .login-overlay .form input[type="text"], diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index e0fb4aeb77..98b8d88869 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -1,34 +1,99 @@ -
-
- - + -
+ + +
  • -
  • -
  • -
-
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html b/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html index c08627739a..7b91125e09 100644 --- a/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html +++ b/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html @@ -1,79 +1,87 @@ - - - - Boot Failed - - - -
- -
-

Boot Failed

-

Umbraco failed to boot, if you are the owner of the website please see the log file for more details.

-
-
- + + + + Boot Failed + + + +
+ +
+

Boot Failed

+

+ Umbraco failed to boot, if you are the owner of the website + please see the log file for more details. +

+
+
+ diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index e1067f31e3..ec4d3c1798 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs index c43754e170..cb9188f5d0 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs @@ -8,7 +8,9 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Routing; @@ -29,6 +31,7 @@ namespace Umbraco.Cms.Web.Website.Controllers { private readonly IMemberManager _memberManager; private readonly ITwoFactorLoginService _twoFactorLoginService; + private readonly IOptions _securitySettings; private readonly ILogger _logger; private readonly IMemberSignInManagerExternalLogins _memberSignInManager; @@ -42,7 +45,8 @@ namespace Umbraco.Cms.Web.Website.Controllers IPublishedUrlProvider publishedUrlProvider, IMemberSignInManagerExternalLogins memberSignInManager, IMemberManager memberManager, - ITwoFactorLoginService twoFactorLoginService) + ITwoFactorLoginService twoFactorLoginService, + IOptions securitySettings) : base( umbracoContextAccessor, databaseFactory, @@ -55,6 +59,7 @@ namespace Umbraco.Cms.Web.Website.Controllers _memberSignInManager = memberSignInManager; _memberManager = memberManager; _twoFactorLoginService = twoFactorLoginService; + _securitySettings = securitySettings; } /// @@ -95,7 +100,7 @@ namespace Umbraco.Cms.Web.Website.Controllers } else { - SignInResult result = await _memberSignInManager.ExternalLoginSignInAsync(loginInfo, false); + SignInResult result = await _memberSignInManager.ExternalLoginSignInAsync(loginInfo, false, _securitySettings.Value.MemberBypassTwoFactorForExternalLogins); if (result == SignInResult.Success) {