* Update projects to .NET 7 * Fix nullability errors * Fix up pipelines to run 7.0 * Update langversion to preview * Revert "Fix up pipelines to run 7.0" This reverts commit d0fa8d01b8126a4eaa59832a3814a567705419ae. * Fix up pipelines again, this time without indentation changes * Include preview versions * Versions not Version * Fix ModelTypeTests * Fix MemberPasswordHasherTests Microsoft wants to use SHA512 instead of SHA256, so our old hashes will return SuccessRehashNeeded now * Use dotnet cli instead of nuget restore * Update src/Umbraco.Web.UI/Umbraco.Web.UI.csproj * Update dependencies * Fix nullability issues * Fix unit test * Fix nullability in ChangingPasswordModel OldPassword can be null, if we're changing the password with password reset enabled. Additionally, we might as well use the new required keyword instead of supressing null. * Use required keyword instead of supressing null * Fix up pipelines again * fix up spelling-error * Use dotnet cli instead of nuget restore * Fix up another NuGet command * Use dotnet version 7 before building * Include preview versions * Remove condition * Use dotnet 7 before running powershell script * Update templates to .net 7 * Download version 7 before running linux container * Move use dotnet 7 even earlier in E2E process * Remove dotnet 7 * Reintroduce .NET 7 task * Update linux docker container and remove dotnet 7 from yml * Fix up dockerfile with ARG * Fix up docker file with nightly builds of dotnet 7 * Reintroduce dotnet 7 so windows can use it * Use aspnet 7 in docker Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch> Co-authored-by: Zeegaan <nge@umbraco.dk>
273 lines
8.8 KiB
C#
273 lines
8.8 KiB
C#
using System.Globalization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Umbraco.Cms.Core.Models;
|
|
using Umbraco.Cms.Core.Services;
|
|
|
|
namespace Umbraco.Cms.Core.Security;
|
|
|
|
/// <summary>
|
|
/// A custom user store that uses Umbraco member data
|
|
/// </summary>
|
|
public class MemberRoleStore : IQueryableRoleStore<UmbracoIdentityRole>
|
|
{
|
|
// TODO: Move into custom error describer.
|
|
// TODO: How revealing can the error messages be?
|
|
private readonly IdentityError _intParseError =
|
|
new() { Code = "IdentityIdParseError", Description = "Cannot parse ID to int" };
|
|
|
|
private readonly IdentityError _memberGroupNotFoundError =
|
|
new() { Code = "IdentityMemberGroupNotFound", Description = "Member group not found" };
|
|
|
|
private readonly IMemberGroupService _memberGroupService;
|
|
|
|
private bool _disposed;
|
|
|
|
// private const string genericIdentityErrorCode = "IdentityErrorUserStore";
|
|
public MemberRoleStore(IMemberGroupService memberGroupService, IdentityErrorDescriber errorDescriber)
|
|
{
|
|
_memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService));
|
|
ErrorDescriber = errorDescriber ?? throw new ArgumentNullException(nameof(errorDescriber));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the <see cref="IdentityErrorDescriber" /> for any error that occurred with the current operation.
|
|
/// </summary>
|
|
public IdentityErrorDescriber ErrorDescriber { get; set; }
|
|
|
|
public IQueryable<UmbracoIdentityRole> Roles =>
|
|
_memberGroupService.GetAll().Select(MapFromMemberGroup).AsQueryable();
|
|
|
|
/// <inheritdoc />
|
|
public Task<IdentityResult> CreateAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
ThrowIfDisposed();
|
|
|
|
if (role == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(role));
|
|
}
|
|
|
|
var memberGroup = new MemberGroup { Name = role.Name };
|
|
|
|
_memberGroupService.Save(memberGroup);
|
|
|
|
role.Id = memberGroup.Id.ToString();
|
|
|
|
return Task.FromResult(IdentityResult.Success);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<IdentityResult> UpdateAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
ThrowIfDisposed();
|
|
|
|
if (role == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(role));
|
|
}
|
|
|
|
if (!int.TryParse(role.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var roleId))
|
|
{
|
|
return Task.FromResult(IdentityResult.Failed(_intParseError));
|
|
}
|
|
|
|
IMemberGroup? memberGroup = _memberGroupService.GetById(roleId);
|
|
if (memberGroup != null)
|
|
{
|
|
if (MapToMemberGroup(role, memberGroup))
|
|
{
|
|
_memberGroupService.Save(memberGroup);
|
|
}
|
|
|
|
return Task.FromResult(IdentityResult.Success);
|
|
}
|
|
|
|
return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<IdentityResult> DeleteAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
ThrowIfDisposed();
|
|
|
|
if (role == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(role));
|
|
}
|
|
|
|
if (!int.TryParse(role.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var roleId))
|
|
{
|
|
throw new ArgumentException("The Id of the role is not an integer");
|
|
}
|
|
|
|
IMemberGroup? memberGroup = _memberGroupService.GetById(roleId);
|
|
if (memberGroup != null)
|
|
{
|
|
_memberGroupService.Delete(memberGroup);
|
|
}
|
|
else
|
|
{
|
|
return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError));
|
|
}
|
|
|
|
return Task.FromResult(IdentityResult.Success);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetRoleIdAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
ThrowIfDisposed();
|
|
|
|
if (role == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(role));
|
|
}
|
|
|
|
return Task.FromResult(role.Id)!;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string?> GetRoleNameAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
ThrowIfDisposed();
|
|
|
|
if (role == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(role));
|
|
}
|
|
|
|
return Task.FromResult(role.Name);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task SetRoleNameAsync(UmbracoIdentityRole role, string? roleName,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
ThrowIfDisposed();
|
|
if (role == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(role));
|
|
}
|
|
|
|
|
|
role.Name = roleName;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string?> GetNormalizedRoleNameAsync(
|
|
UmbracoIdentityRole role,
|
|
CancellationToken cancellationToken = default)
|
|
=> GetRoleNameAsync(role, cancellationToken);
|
|
|
|
/// <inheritdoc />
|
|
public Task SetNormalizedRoleNameAsync(UmbracoIdentityRole role, string? normalizedName,
|
|
CancellationToken cancellationToken = default)
|
|
=> SetRoleNameAsync(role, normalizedName, cancellationToken);
|
|
|
|
/// <inheritdoc />
|
|
public Task<UmbracoIdentityRole?> FindByIdAsync(string roleId, CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
ThrowIfDisposed();
|
|
|
|
if (string.IsNullOrWhiteSpace(roleId))
|
|
{
|
|
throw new ArgumentNullException(nameof(roleId));
|
|
}
|
|
|
|
IMemberGroup? memberGroup;
|
|
|
|
// member group can be found by int or Guid, so try both
|
|
if (!int.TryParse(roleId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id))
|
|
{
|
|
if (!Guid.TryParse(roleId, out Guid guid))
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(roleId), $"{nameof(roleId)} is not a valid Guid");
|
|
}
|
|
|
|
memberGroup = _memberGroupService.GetById(guid);
|
|
}
|
|
else
|
|
{
|
|
memberGroup = _memberGroupService.GetById(id);
|
|
}
|
|
|
|
return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<UmbracoIdentityRole?> FindByNameAsync(string name, CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
ThrowIfDisposed();
|
|
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
throw new ArgumentNullException(nameof(name));
|
|
}
|
|
|
|
IMemberGroup? memberGroup = _memberGroupService.GetByName(name);
|
|
return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup))!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispose the store
|
|
/// </summary>
|
|
public void Dispose() => _disposed = true;
|
|
|
|
/// <summary>
|
|
/// Throws if this class has been disposed.
|
|
/// </summary>
|
|
protected void ThrowIfDisposed()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(GetType().Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a member group to an identity role
|
|
/// </summary>
|
|
/// <param name="memberGroup"></param>
|
|
/// <returns></returns>
|
|
private UmbracoIdentityRole MapFromMemberGroup(IMemberGroup memberGroup)
|
|
{
|
|
// NOTE: there is a ConcurrencyStamp property but we don't use it. The purpose
|
|
// of this value is to try to prevent concurrent writes in the DB but this is
|
|
// an implementation detail at the data source level that has leaked into the
|
|
// model. A good writeup of that is here:
|
|
// https://stackoverflow.com/a/37362173
|
|
// For our purposes currently we won't worry about this.
|
|
var result = new UmbracoIdentityRole { Id = memberGroup.Id.ToString(), Name = memberGroup.Name };
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Map an identity role to a member group
|
|
/// </summary>
|
|
/// <param name="role"></param>
|
|
/// <param name="memberGroup"></param>
|
|
/// <returns></returns>
|
|
private bool MapToMemberGroup(UmbracoIdentityRole role, IMemberGroup memberGroup)
|
|
{
|
|
var anythingChanged = false;
|
|
|
|
if (role.IsPropertyDirty(nameof(UmbracoIdentityRole.Name))
|
|
&& !string.IsNullOrEmpty(role.Name) && memberGroup.Name != role.Name)
|
|
{
|
|
memberGroup.Name = role.Name;
|
|
anythingChanged = true;
|
|
}
|
|
|
|
return anythingChanged;
|
|
}
|
|
}
|