Merge remote-tracking branch 'origin/release/12.3.4' into v12/dev

This commit is contained in:
Bjarke Berg
2023-12-11 14:37:36 +01:00
15 changed files with 87 additions and 100 deletions

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
@@ -11,20 +12,20 @@ namespace Umbraco.Cms.Api.Delivery.Handlers;
internal sealed class InitializeMemberApplicationNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStartingNotification>
{
private readonly IMemberApplicationManager _memberApplicationManager;
private readonly IRuntimeState _runtimeState;
private readonly ILogger<InitializeMemberApplicationNotificationHandler> _logger;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly IServiceScopeFactory _serviceScopeFactory;
public InitializeMemberApplicationNotificationHandler(
IMemberApplicationManager memberApplicationManager,
IRuntimeState runtimeState,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<InitializeMemberApplicationNotificationHandler> logger)
ILogger<InitializeMemberApplicationNotificationHandler> logger,
IServiceScopeFactory serviceScopeFactory)
{
_memberApplicationManager = memberApplicationManager;
_runtimeState = runtimeState;
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_deliveryApiSettings = deliveryApiSettings.Value;
}
@@ -35,26 +36,31 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
return;
}
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
// install that is not allowed (no connection string means no DbContext)
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
{
await _memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
return;
}
if (ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls) is false)
{
await _memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
return;
}
if (_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls.Any()
&& ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls) is false)
{
await _memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
return;
}
await _memberApplicationManager.EnsureMemberApplicationAsync(
await memberApplicationManager.EnsureMemberApplicationAsync(
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls,
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls,
cancellationToken);

View File

@@ -1,14 +1,10 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Serilog;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DistributedLocking;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Persistence.EFCore.Factories;
using Umbraco.Cms.Persistence.EFCore.Locking;
using Umbraco.Cms.Persistence.EFCore.Scoping;
@@ -22,13 +18,6 @@ public static class UmbracoEFCoreServiceCollectionExtensions
public static IServiceCollection AddUmbracoEFCoreContext<T>(this IServiceCollection services, DefaultEFCoreOptionsAction? defaultEFCoreOptionsAction = null)
where T : DbContext
{
var optionsBuilder = new DbContextOptionsBuilder<T>();
services.TryAddSingleton<IDbContextFactory<T>>(
sp =>
{
SetupDbContext(defaultEFCoreOptionsAction, sp, optionsBuilder);
return new UmbracoPooledDbContextFactory<T>(sp.GetRequiredService<IRuntimeState>(), optionsBuilder.Options);
});
services.AddPooledDbContextFactory<T>((provider, builder) => SetupDbContext(defaultEFCoreOptionsAction, provider, builder));
services.AddTransient(services => services.GetRequiredService<IDbContextFactory<T>>().CreateDbContext());
@@ -52,13 +41,6 @@ public static class UmbracoEFCoreServiceCollectionExtensions
connectionString = connectionString.Replace(Constants.System.DataDirectoryPlaceholder, dataDirectory);
}
var optionsBuilder = new DbContextOptionsBuilder<T>();
services.TryAddSingleton<IDbContextFactory<T>>(
sp =>
{
defaultEFCoreOptionsAction?.Invoke(optionsBuilder, providerName, connectionString);
return new UmbracoPooledDbContextFactory<T>(sp.GetRequiredService<IRuntimeState>(), optionsBuilder.Options);
});
services.AddPooledDbContextFactory<T>(options => defaultEFCoreOptionsAction?.Invoke(options, providerName, connectionString));
services.AddTransient(services => services.GetRequiredService<IDbContextFactory<T>>().CreateDbContext());
@@ -99,13 +81,6 @@ public static class UmbracoEFCoreServiceCollectionExtensions
{
optionsAction ??= (sp, options) => { };
var optionsBuilder = new DbContextOptionsBuilder<T>();
services.TryAddSingleton<IDbContextFactory<T>>(sp =>
{
optionsAction.Invoke(sp, optionsBuilder);
return new UmbracoPooledDbContextFactory<T>(sp.GetRequiredService<IRuntimeState>(), optionsBuilder.Options);
});
services.AddPooledDbContextFactory<T>(optionsAction);
services.AddTransient(services => services.GetRequiredService<IDbContextFactory<T>>().CreateDbContext());

View File

@@ -1,47 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Persistence.EFCore.Factories;
/// <inheritdoc/>
internal class UmbracoPooledDbContextFactory<TContext> : PooledDbContextFactory<TContext>
where TContext : DbContext
{
private readonly IRuntimeState _runtimeState;
private readonly DbContextOptions<TContext> _options;
/// <inheritdoc/>
public UmbracoPooledDbContextFactory(IRuntimeState runtimeState, DbContextOptions<TContext> options, int poolSize = 1024 /*DbContextPool<DbContext>.DefaultPoolSize*/) : base(options, poolSize)
{
_runtimeState = runtimeState;
_options = options;
}
/// <inheritdoc/>
public override TContext CreateDbContext()
{
if (_runtimeState.Level == RuntimeLevel.Run)
{
return base.CreateDbContext();
}
else
{
return (TContext?)Activator.CreateInstance(typeof(TContext), _options) ?? throw new InvalidOperationException("Unable to create DbContext");
}
}
/// <inheritdoc/>
public override async Task<TContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
{
if (_runtimeState.Level == RuntimeLevel.Run)
{
return await base.CreateDbContextAsync(cancellationToken);
}
else
{
return (TContext?)Activator.CreateInstance(typeof(TContext), _options) ?? throw new InvalidOperationException("Unable to create DbContext");
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.DependencyInjection;
@@ -55,6 +56,10 @@ public class UmbracoDbContext : DbContext
{
ILogger<UmbracoDbContext> logger = StaticServiceProvider.Instance.GetRequiredService<ILogger<UmbracoDbContext>>();
logger.LogCritical("No connection string was found, cannot setup Umbraco EF Core context");
// we're throwing an exception here to make it abundantly clear that one should never utilize (or have a
// dependency on) the DbContext before the connection string has been initialized by the installer.
throw new ConfigurationErrorsException("No connection string was found, cannot setup Umbraco EF Core context");
}
IEnumerable<IMigrationProviderSetup> migrationProviders = StaticServiceProvider.Instance.GetServices<IMigrationProviderSetup>();

View File

@@ -66,7 +66,17 @@ public class ExamineIndexRebuilder : IIndexRebuilder
_logger.LogInformation("Starting async background thread for rebuilding index {indexName}.", indexName);
_backgroundTaskQueue.QueueBackgroundWorkItem(
cancellationToken => Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken)));
cancellationToken =>
{
// Do not flow AsyncLocal to the child thread
using (ExecutionContext.SuppressFlow())
{
Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken));
// immediately return so the queue isn't waiting.
return Task.CompletedTask;
}
});
}
else
{
@@ -96,12 +106,16 @@ public class ExamineIndexRebuilder : IIndexRebuilder
_backgroundTaskQueue.QueueBackgroundWorkItem(
cancellationToken =>
{
// This is a fire/forget task spawned by the background thread queue (which means we
// don't need to worry about ExecutionContext flowing).
Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken));
// Do not flow AsyncLocal to the child thread
using (ExecutionContext.SuppressFlow())
{
// This is a fire/forget task spawned by the background thread queue (which means we
// don't need to worry about ExecutionContext flowing).
Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken));
// immediately return so the queue isn't waiting.
return Task.CompletedTask;
// immediately return so the queue isn't waiting.
return Task.CompletedTask;
}
});
}
else

View File

@@ -311,7 +311,12 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository
definition.Name.Replace(' ', '_')));
Directory.CreateDirectory(directoryName);
var expectedRoot = _hostingEnvironment.MapPathContentRoot(_createdPackagesFolderPath);
var finalPackagePath = Path.Combine(directoryName, fileName);
if (finalPackagePath.StartsWith(expectedRoot) == false)
{
throw new IOException("Invalid path due to the package name");
}
// Clean existing files
foreach (var packagePath in new[] { definition.PackagePath, finalPackagePath })

View File

@@ -1,9 +1,12 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
public class AnalyticsController : UmbracoAuthorizedJsonController
{
private readonly IMetricsConsentService _metricsConsentService;

View File

@@ -404,6 +404,9 @@ public class AuthenticationController : UmbracoApiControllerBase
}
BackOfficeIdentityUser? identityUser = await _userManager.FindByEmailAsync(model.Email);
await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500)); // To randomize response time preventing user enumeration
if (identityUser != null)
{
IUser? user = _userService.GetByEmail(model.Email);
@@ -424,14 +427,20 @@ public class AuthenticationController : UmbracoApiControllerBase
var mailMessage = new EmailMessage(from, user.Email, subject, message, true);
await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.PasswordReset, true);
try
{
await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.PasswordReset, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending email, please check your SMTP configuration: {ErrorMessage}", ex.Message);
return Ok();
}
_userManager.NotifyForgotPasswordRequested(User, user.Id.ToString());
}
}
await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500));
return Ok();
}

View File

@@ -18,6 +18,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers;
/// Backoffice controller supporting the dashboard for language administration.
/// </summary>
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
public class LanguageController : UmbracoAuthorizedJsonController
{
private readonly ILocalizationService _localizationService;

View File

@@ -1,13 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
public class PublishedSnapshotCacheStatusController : UmbracoAuthorizedApiController
{
private readonly DistributedCache _distributedCache;

View File

@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -14,11 +15,13 @@ using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
[Authorize(Policy = AuthorizationPolicies.SectionAccessContent)]
public class RedirectUrlManagementController : UmbracoAuthorizedApiController
{
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
@@ -45,6 +48,8 @@ public class RedirectUrlManagementController : UmbracoAuthorizedApiController
_configManipulator = configManipulator ?? throw new ArgumentNullException(nameof(configManipulator));
}
private bool IsEnabled => _webRoutingSettings.CurrentValue.DisableRedirectUrlTracking == false;
/// <summary>
/// Returns true/false of whether redirect tracking is enabled or not
/// </summary>
@@ -52,9 +57,8 @@ public class RedirectUrlManagementController : UmbracoAuthorizedApiController
[HttpGet]
public IActionResult GetEnableState()
{
var enabled = _webRoutingSettings.CurrentValue.DisableRedirectUrlTracking == false;
var userIsAdmin = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.IsAdmin() ?? false;
return Ok(new { enabled, userIsAdmin });
return Ok(new { enabled = IsEnabled, userIsAdmin });
}
//add paging
@@ -104,6 +108,11 @@ public class RedirectUrlManagementController : UmbracoAuthorizedApiController
[HttpPost]
public IActionResult DeleteRedirectUrl(Guid id)
{
if (IsEnabled is false)
{
return BadRequest("Redirect URL tracking is disabled, and therefore no URLs can be deleted.");
}
_redirectUrlService.Delete(id);
return Ok();
}

View File

@@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
using Stylesheet = Umbraco.Cms.Core.Models.ContentEditing.Stylesheet;
@@ -12,6 +14,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers;
/// The API controller used for retrieving available stylesheets
/// </summary>
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
public class StylesheetController : UmbracoAuthorizedJsonController
{
private readonly IFileService _fileService;

View File

@@ -28,7 +28,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
* });
* </pre>
* @returns {Promise} resourcePromise object
*
*
*/
get2FAProviders: function () {
@@ -203,7 +203,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
"PostRequestPasswordReset"), {
email: email
}),
'Request password reset failed for email ' + email);
'An email with password reset instructions will be sent to the specified address if it matched our records');
},
/**

View File

@@ -11,11 +11,12 @@ angular.module('umbraco').controller("Umbraco.LoginController", function (events
//check if there's a returnPath query string, if so redirect to it
var locationObj = $location.search();
if (locationObj.returnPath) {
// decodeURIComponent(...) does not play nice with OAuth redirect URLs, so until we have a
// dedicated login screen for the new back-office, we need to hardcode this exception
path = locationObj.returnPath.indexOf("/security/back-office/authorize") > 0
? locationObj.returnPath
: decodeURIComponent(locationObj.returnPath);
// ensure that the returnPath is a valid URL under the current origin (prevents DOM-XSS among other things)
const returnPath = decodeURIComponent(locationObj.returnPath);
const url = new URL(returnPath, window.location.origin);
if (url.origin === window.location.origin) {
path = returnPath;
}
}
// Ensure path is not absolute

View File

@@ -158,7 +158,7 @@
</div>
<div ng-messages="vm.requestPasswordResetForm.$error" class="control-group" ng-show="vm.requestPasswordResetForm.$invalid">
<p ng-message="auth" class="text-error" role="alert" tabindex="0">{{vm.errorMsg}}</p>
<p ng-message="auth" class="text-info" role="alert" tabindex="0">{{vm.errorMsg}}</p>
</div>
<div class="control-group" ng-show="vm.showEmailResetConfirmation">