Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public static IUmbracoBuilder AddBackOfficeIdentity(this IUmbracoBuilder builder
factory.GetRequiredService<IUserRepository>(),
factory.GetRequiredService<IRuntimeState>(),
factory.GetRequiredService<IEventMessagesFactory>(),
factory.GetRequiredService<ILogger<BackOfficeUserStore>>()))
factory.GetRequiredService<ILogger<BackOfficeUserStore>>(),
factory.GetRequiredService<IBackOfficeUserReader>()))
.AddUserManager<IBackOfficeUserManager, BackOfficeUserManager>()
.AddSignInManager<IBackOfficeSignInManager, BackOfficeSignInManager>()
.AddClaimsPrincipalFactory<BackOfficeClaimsPrincipalFactory>()
Expand Down
49 changes: 49 additions & 0 deletions src/Umbraco.Core/Services/IBackOfficeUserReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Umbraco.Cms.Core.Models.Membership;

namespace Umbraco.Cms.Core.Services;

/// <summary>
/// Provides shared read access to back office users for both <see cref="IUserService"/> and <see cref="Security.IBackOfficeUserStore"/>.
/// </summary>
/// <remarks>
/// Centralises the repository calls so the two consumers cannot drift. Registered independently of
/// <see cref="Security.IBackOfficeUserStore"/> so it is available in delivery-only setups.
/// </remarks>
public interface IBackOfficeUserReader
{
/// <summary>
/// Retrieves a back office user by their integer identifier, falling back to a minimal upgrade-safe query
/// if the user table schema is mid-migration.
/// </summary>
/// <param name="id">The integer identifier of the user.</param>
/// <returns>The <see cref="IUser"/> if found; otherwise, <c>null</c>.</returns>
IUser? GetById(int id);

/// <summary>
/// Retrieves a back office user by their unique key.
/// </summary>
/// <param name="key">The unique key of the user.</param>
/// <returns>The <see cref="IUser"/> if found; otherwise, <c>null</c>.</returns>
IUser? GetByKey(Guid key);

/// <summary>
/// Retrieves the back office users with the specified integer identifiers.
/// </summary>
/// <param name="ids">The integer identifiers of the users to retrieve.</param>
/// <returns>The matching users, or an empty collection if <paramref name="ids"/> is empty.</returns>
IEnumerable<IUser> GetManyById(IEnumerable<int> ids);

/// <summary>
/// Retrieves the back office users with the specified unique keys.
/// </summary>
/// <param name="keys">The unique keys of the users to retrieve.</param>
/// <returns>The matching users, or an empty collection if <paramref name="keys"/> is empty.</returns>
IEnumerable<IUser> GetManyByKey(IEnumerable<Guid> keys);

/// <summary>
/// Retrieves all back office users that belong to the specified user group.
/// </summary>
/// <param name="groupId">The integer identifier of the user group.</param>
/// <returns>The users in the group, or an empty collection if the group has no members.</returns>
IEnumerable<IUser> GetAllInGroup(int groupId);
}
39 changes: 10 additions & 29 deletions src/Umbraco.Core/Services/UserService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;

Check notice on line 1 in src/Umbraco.Core/Services/UserService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Lines of Code in a Single File

The lines of code decreases from 1703 to 1687, improve code health by reducing it to 1000. The number of Lines of Code in a single file. More Lines of Code lowers the code health.

Check notice on line 1 in src/Umbraco.Core/Services/UserService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Code Duplication

reduced similar code in: GetAllInGroup. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.

Check notice on line 1 in src/Umbraco.Core/Services/UserService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Primitive Obsession

The ratio of primitive types in function arguments decreases from 60.00% to 59.71%, threshold = 30.0%. The functions in this file have too many primitive types (e.g. int, double, float) in their function argument lists. Using many primitive types lead to the code smell Primitive Obsession. Avoid adding more primitive arguments.
using System.Linq.Expressions;
using System.Security.Claims;
using System.Security.Cryptography;
Expand Down Expand Up @@ -51,6 +51,7 @@
private readonly IUserRepository _userRepository;
private readonly ContentSettings _contentSettings;
private readonly IUserIdKeyResolver _userIdKeyResolver;
private readonly IBackOfficeUserReader _backOfficeUserReader;

/// <summary>
/// Initializes a new instance of the <see cref="UserService" /> class.
Expand All @@ -74,6 +75,7 @@
/// <param name="isoCodeValidator">The validator for ISO codes.</param>
/// <param name="forgotPasswordSender">The sender for forgot password emails.</param>
/// <param name="userIdKeyResolver">The resolver for user ID to key mappings.</param>
/// <param name="backOfficeUserReader">The shared reader used for back office user lookups.</param>
public UserService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
Expand All @@ -93,7 +95,8 @@
IOptions<ContentSettings> contentSettings,
IIsoCodeValidator isoCodeValidator,
IUserForgotPasswordSender forgotPasswordSender,
IUserIdKeyResolver userIdKeyResolver)
IUserIdKeyResolver userIdKeyResolver,
IBackOfficeUserReader backOfficeUserReader)
: base(provider, loggerFactory, eventMessagesFactory)
{
_userRepository = userRepository;
Expand All @@ -109,6 +112,7 @@
_isoCodeValidator = isoCodeValidator;
_forgotPasswordSender = forgotPasswordSender;
_userIdKeyResolver = userIdKeyResolver;
_backOfficeUserReader = backOfficeUserReader;
_globalSettings = globalSettings.Value;
_securitySettings = securitySettings.Value;
_contentSettings = contentSettings.Value;
Expand Down Expand Up @@ -1695,10 +1699,7 @@
return Array.Empty<IUser>();
}

using IServiceScope scope = _serviceScopeFactory.CreateScope();
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();

return backOfficeUserStore.GetAllInGroupAsync(groupId.Value).GetAwaiter().GetResult();
return _backOfficeUserReader.GetAllInGroup(groupId.Value);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -1739,30 +1740,15 @@

/// <inheritdoc/>
public IUser? GetUserById(int id)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();

return backOfficeUserStore.GetAsync(id).GetAwaiter().GetResult();
}
=> _backOfficeUserReader.GetById(id);

/// <inheritdoc/>
public Task<IUser?> GetAsync(Guid key)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();

return backOfficeUserStore.GetAsync(key);
}
=> Task.FromResult(_backOfficeUserReader.GetByKey(key));

/// <inheritdoc/>
public Task<IEnumerable<IUser>> GetAsync(IEnumerable<Guid> keys)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();

return backOfficeUserStore.GetUsersAsync(keys.ToArray());
}
=> Task.FromResult(_backOfficeUserReader.GetManyByKey(keys));

/// <inheritdoc/>
public async Task<Attempt<ICollection<IIdentityUserLogin>, UserOperationStatus>> GetLinkedLoginsAsync(Guid userKey)
Expand All @@ -1787,12 +1773,7 @@

/// <inheritdoc/>
public IEnumerable<IUser> GetUsersById(params int[]? ids)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();

return backOfficeUserStore.GetUsersAsync(ids).GetAwaiter().GetResult();
}
=> ids is null ? Enumerable.Empty<IUser>() : _backOfficeUserReader.GetManyById(ids);

/// <inheritdoc/>
public void ReplaceUserGroupPermissions(int groupId, ISet<string> permissions, params int[] entityIds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Implement;
Expand Down Expand Up @@ -43,6 +44,7 @@ internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder)
// register the special idk map
builder.Services.AddUnique<IIdKeyMap, IdKeyMap>();
builder.Services.AddUnique<IUserIdKeyResolver, UserIdKeyResolver>();
builder.Services.AddUnique<IBackOfficeUserReader, BackOfficeUserReader>();

builder.Services.AddUnique<IAuditService, AuditService>();
builder.Services.AddUnique<IAuditEntryService, AuditEntryService>();
Expand Down
103 changes: 103 additions & 0 deletions src/Umbraco.Infrastructure/Security/BackOfficeUserReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Data.Common;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;

namespace Umbraco.Cms.Core.Security;

/// <summary>
/// Default implementation of <see cref="IBackOfficeUserReader"/>; wraps <see cref="IUserRepository"/>
/// with the scope handling and upgrade-state fallback required by back office user lookups.
/// </summary>
internal sealed class BackOfficeUserReader : IBackOfficeUserReader
{
private readonly ICoreScopeProvider _scopeProvider;
private readonly IUserRepository _userRepository;
private readonly IRuntimeState _runtimeState;

/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeUserReader"/> class.
/// </summary>
/// <param name="scopeProvider">Provides database transaction scopes for data operations.</param>
/// <param name="userRepository">Repository for accessing user data.</param>
/// <param name="runtimeState">Represents the current runtime state of the Umbraco application.</param>
public BackOfficeUserReader(
ICoreScopeProvider scopeProvider,
IUserRepository userRepository,
IRuntimeState runtimeState)
{
_scopeProvider = scopeProvider;
_userRepository = userRepository;
_runtimeState = runtimeState;
}

/// <inheritdoc />
public IUser? GetById(int id)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);

try
{
return _userRepository.Get(id);
}
catch (DbException)
{
// During upgrades the user table schema may be mid-migration, so fall back to
// a minimal query that only resolves the fields needed for authorization.
if (IsUpgrading)
{
return _userRepository.GetForUpgrade(id);
}

throw;
}
}

/// <inheritdoc />
public IUser? GetByKey(Guid key)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
return _userRepository.Get(key);
}

/// <inheritdoc />
public IEnumerable<IUser> GetManyById(IEnumerable<int> ids)
{
// Need to use a List here because the expression tree cannot convert the array when used in Contains.
// See ExpressionTests.Sql_In().
List<int> idsAsList = [.. ids];
if (idsAsList.Count == 0)
{
return Enumerable.Empty<IUser>();
}

using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IUser> query = _scopeProvider.CreateQuery<IUser>().Where(x => idsAsList.Contains(x.Id));
return _userRepository.Get(query);
}

/// <inheritdoc />
public IEnumerable<IUser> GetManyByKey(IEnumerable<Guid> keys)
{
Guid[] keysArray = keys as Guid[] ?? keys.ToArray();
if (keysArray.Length == 0)
{
return Enumerable.Empty<IUser>();
}

using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
return _userRepository.GetMany(keysArray);
}

/// <inheritdoc />
public IEnumerable<IUser> GetAllInGroup(int groupId)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
return _userRepository.GetAllInGroup(groupId);
}

private bool IsUpgrading =>
_runtimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading;
}
Loading
Loading