Implements Public Access in netcore (#10137)

* Getting new netcore PublicAccessChecker in place

* Adds full test coverage for PublicAccessChecker

* remove PublicAccessComposer

* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller

* Implements the required methods on IMemberManager, removes old migrated code

* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops

* adds note

* adds note

* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.

* Changes name to IUmbracoEndpointBuilder

* adds note

* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect

* fixing build

* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.

* adds note

* removes unused filter, fixes build

* fixes WebPath and tests

* Looks up entities in one query

* remove usings

* Fix test, remove stylesheet

* Set status code before we write to response to avoid error

* Ensures that users and members are validated when logging in. Shares more code between users and members.

* Fixes RepositoryCacheKeys to ensure the keys are normalized

* oops didn't mean to commit this

* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy

* bah, far out this keeps getting recommitted. sorry

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Shannon Deminick
2021-04-20 15:11:45 +10:00
committed by GitHub
parent 385cc62523
commit a1624d26a3
150 changed files with 2715 additions and 2173 deletions

View File

@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
@@ -24,7 +24,7 @@ namespace Umbraco.Cms.Core.Cache
public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyBase<TEntity, TId>
where TEntity : class, IEntity
{
private static readonly TEntity[] EmptyEntities = new TEntity[0]; // const
private static readonly TEntity[] s_emptyEntities = new TEntity[0]; // const
private readonly RepositoryCachePolicyOptions _options;
public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options)
@@ -33,21 +33,29 @@ namespace Umbraco.Cms.Core.Cache
_options = options ?? throw new ArgumentNullException(nameof(options));
}
protected string GetEntityCacheKey(object id)
protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id;
protected string GetEntityCacheKey(TId id)
{
if (id == null) throw new ArgumentNullException(nameof(id));
return GetEntityTypeCacheKey() + id;
if (EqualityComparer<TId>.Default.Equals(id, default))
{
return string.Empty;
}
if (typeof(TId).IsValueType)
{
return EntityTypeCacheKey + id;
}
else
{
return EntityTypeCacheKey + id.ToString().ToUpperInvariant();
}
}
protected string GetEntityTypeCacheKey()
{
return $"uRepo_{typeof (TEntity).Name}_";
}
protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_";
protected virtual void InsertEntity(string cacheKey, TEntity entity)
{
Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true);
}
=> Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true);
protected virtual void InsertEntities(TId[] ids, TEntity[] entities)
{
@@ -56,7 +64,7 @@ namespace Umbraco.Cms.Core.Cache
// getting all of them, and finding nothing.
// if we can cache a zero count, cache an empty array,
// for as long as the cache is not cleared (no expiration)
Cache.Insert(GetEntityTypeCacheKey(), () => EmptyEntities);
Cache.Insert(EntityTypeCacheKey, () => s_emptyEntities);
}
else
{
@@ -85,7 +93,7 @@ namespace Umbraco.Cms.Core.Cache
}
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache.Clear(GetEntityTypeCacheKey());
Cache.Clear(EntityTypeCacheKey);
}
catch
{
@@ -95,7 +103,7 @@ namespace Umbraco.Cms.Core.Cache
Cache.Clear(GetEntityCacheKey(entity.Id));
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache.Clear(GetEntityTypeCacheKey());
Cache.Clear(EntityTypeCacheKey);
throw;
}
@@ -117,7 +125,7 @@ namespace Umbraco.Cms.Core.Cache
}
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache.Clear(GetEntityTypeCacheKey());
Cache.Clear(EntityTypeCacheKey);
}
catch
{
@@ -127,7 +135,7 @@ namespace Umbraco.Cms.Core.Cache
Cache.Clear(GetEntityCacheKey(entity.Id));
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache.Clear(GetEntityTypeCacheKey());
Cache.Clear(EntityTypeCacheKey);
throw;
}
@@ -148,7 +156,7 @@ namespace Umbraco.Cms.Core.Cache
var cacheKey = GetEntityCacheKey(entity.Id);
Cache.Clear(cacheKey);
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache.Clear(GetEntityTypeCacheKey());
Cache.Clear(EntityTypeCacheKey);
}
}
@@ -160,11 +168,16 @@ namespace Umbraco.Cms.Core.Cache
// if found in cache then return else fetch and cache
if (fromCache != null)
{
return fromCache;
}
var entity = performGet(id);
if (entity != null && entity.HasIdentity)
{
InsertEntity(cacheKey, entity);
}
return entity;
}
@@ -199,7 +212,7 @@ namespace Umbraco.Cms.Core.Cache
else
{
// get everything we have
var entities = Cache.GetCacheItemsByKeySearch<TEntity>(GetEntityTypeCacheKey())
var entities = Cache.GetCacheItemsByKeySearch<TEntity>(EntityTypeCacheKey)
.ToArray(); // no need for null checks, we are not caching nulls
if (entities.Length > 0)
@@ -222,7 +235,7 @@ namespace Umbraco.Cms.Core.Cache
{
// if none of them were in the cache
// and we allow zero count - check for the special (empty) entry
var empty = Cache.GetCacheItem<TEntity[]>(GetEntityTypeCacheKey());
var empty = Cache.GetCacheItem<TEntity[]>(EntityTypeCacheKey);
if (empty != null) return empty;
}
}
@@ -242,7 +255,7 @@ namespace Umbraco.Cms.Core.Cache
/// <inheritdoc />
public override void ClearAll()
{
Cache.ClearByKey(GetEntityTypeCacheKey());
Cache.ClearByKey(EntityTypeCacheKey);
}
}
}

View File

@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Handlers;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services.Notifications;

View File

@@ -2,14 +2,15 @@
// See LICENSE for more details.
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Runtime;
using Umbraco.Cms.Core.Sync;
using Umbraco.Extensions;
@@ -85,21 +86,18 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
using (_profilingLogger.DebugDuration<KeepAlive>("Keep alive executing", "Keep alive complete"))
{
var keepAlivePingUrl = _keepAliveSettings.KeepAlivePingUrl;
var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl.ToString();
if (umbracoAppUrl.IsNullOrWhiteSpace())
{
_logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
return;
}
// If the config is an absolute path, just use it
string keepAlivePingUrl = WebPath.Combine(umbracoAppUrl, _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl));
try
{
if (keepAlivePingUrl.Contains("{umbracoApplicationUrl}"))
{
var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl.ToString();
if (umbracoAppUrl.IsNullOrWhiteSpace())
{
_logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
return;
}
keepAlivePingUrl = keepAlivePingUrl.Replace("{umbracoApplicationUrl}", umbracoAppUrl.TrimEnd(Constants.CharArrays.ForwardSlash));
}
var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl);
HttpClient httpClient = _httpClientFactory.CreateClient();
_ = await httpClient.SendAsync(request);

View File

@@ -1,5 +1,5 @@
using System;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Persistence.Factories

View File

@@ -1,5 +1,5 @@
using System;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Persistence.Mappers

View File

@@ -1,5 +1,5 @@
using System;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Persistence.Mappers

View File

@@ -85,7 +85,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
Database.Update(dto);
entity.ResetDirtyProperties();
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IConsent>(entity.Id));
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IConsent, int>(entity.Id));
}
/// <inheritdoc />

View File

@@ -176,8 +176,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
entity.ResetDirtyProperties();
//Clear the cache entries that exist by uniqueid/item key
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem>(entity.ItemKey));
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem>(entity.Key));
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem, string>(entity.ItemKey));
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem, Guid>(entity.Key));
}
protected override void PersistDeletedItem(IDictionaryItem entity)
@@ -188,8 +188,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
Database.Delete<DictionaryDto>("WHERE id = @Id", new { Id = entity.Key });
//Clear the cache entries that exist by uniqueid/item key
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem>(entity.ItemKey));
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem>(entity.Key));
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem, string>(entity.ItemKey));
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem, Guid>(entity.Key));
entity.DeleteDate = DateTime.Now;
}
@@ -205,8 +205,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
Database.Delete<DictionaryDto>("WHERE id = @Id", new { Id = dto.UniqueId });
//Clear the cache entries that exist by uniqueid/item key
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem>(dto.Key));
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem>(dto.UniqueId));
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem, string>(dto.Key));
IsolatedCache.Clear(RepositoryCacheKeys.GetKey<IDictionaryItem, Guid>(dto.UniqueId));
}
}

View File

@@ -1176,7 +1176,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
if (withCache)
{
// if the cache contains the (proper version of the) item, use it
var cached = IsolatedCache.GetCacheItem<IContent>(RepositoryCacheKeys.GetKey<IContent>(dto.NodeId));
var cached = IsolatedCache.GetCacheItem<IContent>(RepositoryCacheKeys.GetKey<IContent, int>(dto.NodeId));
if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id)
{
content[i] = (Content)cached;

View File

@@ -5,11 +5,11 @@ using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Factories;
using Umbraco.Cms.Infrastructure.Persistence.Querying;

View File

@@ -511,7 +511,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
if (withCache)
{
// if the cache contains the (proper version of the) item, use it
var cached = IsolatedCache.GetCacheItem<IMedia>(RepositoryCacheKeys.GetKey<IMedia>(dto.NodeId));
var cached = IsolatedCache.GetCacheItem<IMedia>(RepositoryCacheKeys.GetKey<IMedia, int>(dto.NodeId));
if (cached != null && cached.VersionId == dto.ContentVersionDto.Id)
{
content[i] = (Core.Models.Media) cached;

View File

@@ -615,7 +615,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
if (withCache)
{
// if the cache contains the (proper version of the) item, use it
var cached = IsolatedCache.GetCacheItem<IMember>(RepositoryCacheKeys.GetKey<IMember>(dto.NodeId));
var cached = IsolatedCache.GetCacheItem<IMember>(RepositoryCacheKeys.GetKey<IMember, int>(dto.NodeId));
if (cached != null && cached.VersionId == dto.ContentVersionDto.Id)
{
content[i] = (Member)cached;

View File

@@ -86,6 +86,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
protected override IUser PerformGet(int id)
{
// This will never resolve to a user, yet this is asked
// for all of the time (especially in cases of members).
// Don't issue a SQL call for this, we know it will not exist.
if (id == default || id < -1)
{
return null;
}
var sql = SqlContext.Sql()
.Select<UserDto>()
.From<UserDto>()

View File

@@ -5,7 +5,12 @@ namespace Umbraco.Cms.Core.Security
/// <summary>
/// Umbraco back office specific <see cref="IdentityErrorDescriber"/>
/// </summary>
public class BackOfficeIdentityErrorDescriber : IdentityErrorDescriber
public class BackOfficeErrorDescriber : IdentityErrorDescriber
{
// TODO: Override all the methods in order to provide our own translated error messages
}
public class MembersErrorDescriber : IdentityErrorDescriber
{
// TODO: Override all the methods in order to provide our own translated error messages
}

View File

@@ -41,17 +41,17 @@ namespace Umbraco.Cms.Core.Security
services => new BackOfficePasswordHasher(
new LegacyPasswordSecurity(),
services.GetRequiredService<IJsonSerializer>()));
services.TryAddScoped<IUserConfirmation<BackOfficeIdentityUser>, DefaultUserConfirmation<BackOfficeIdentityUser>>();
services.TryAddScoped<IUserConfirmation<BackOfficeIdentityUser>, UmbracoUserConfirmation<BackOfficeIdentityUser>>();
}
// override to add itself, by default identity only wants a single IdentityErrorDescriber
public override IdentityBuilder AddErrorDescriber<TDescriber>()
{
if (!typeof(BackOfficeIdentityErrorDescriber).IsAssignableFrom(typeof(TDescriber)))
if (!typeof(BackOfficeErrorDescriber).IsAssignableFrom(typeof(TDescriber)))
{
throw new InvalidOperationException($"The type {typeof(TDescriber)} does not inherit from {typeof(BackOfficeIdentityErrorDescriber)}");
throw new InvalidOperationException($"The type {typeof(TDescriber)} does not inherit from {typeof(BackOfficeErrorDescriber)}");
}
// Add as itself, by default identity only wants a single IdentityErrorDescriber
Services.AddScoped<TDescriber>();
return this;
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Extensions;
@@ -14,8 +13,6 @@ namespace Umbraco.Cms.Core.Security
/// </summary>
public class BackOfficeIdentityUser : UmbracoIdentityUser
{
private string _name;
private string _passwordConfig;
private string _culture;
private IReadOnlyCollection<IReadOnlyUserGroup> _groups;
private string[] _allowedSections;
@@ -55,7 +52,7 @@ namespace Umbraco.Cms.Core.Security
user.Id = null;
user.HasIdentity = false;
user._culture = culture;
user._name = name;
user.Name = name;
user.EnableChangeTracking();
return user;
}
@@ -84,25 +81,6 @@ namespace Umbraco.Cms.Core.Security
public int[] CalculatedMediaStartNodeIds { get; set; }
public int[] CalculatedContentStartNodeIds { get; set; }
/// <summary>
/// Gets or sets the user's real name
/// </summary>
public string Name
{
get => _name;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
}
/// <summary>
/// Gets or sets the password config
/// </summary>
public string PasswordConfig
{
get => _passwordConfig;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
}
/// <summary>
/// Gets or sets content start nodes assigned to the User (not ones assigned to the user's groups)
/// </summary>
@@ -181,23 +159,6 @@ namespace Umbraco.Cms.Core.Security
}
}
/// <summary>
/// Gets a value indicating whether the user is locked out based on the user's lockout end date
/// </summary>
public bool IsLockedOut
{
get
{
bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
return isLocked;
}
}
/// <summary>
/// Gets or sets a value indicating whether the IUser IsApproved
/// </summary>
public bool IsApproved { get; set; }
private static string UserIdToString(int userId) => string.Intern(userId.ToString());
}
}

View File

@@ -8,12 +8,10 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
@@ -46,7 +44,7 @@ namespace Umbraco.Cms.Core.Security
IExternalLoginService externalLoginService,
IOptions<GlobalSettings> globalSettings,
UmbracoMapper mapper,
BackOfficeIdentityErrorDescriber describer,
BackOfficeErrorDescriber describer,
AppCaches appCaches)
: base(describer)
{

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Umbraco.Cms.Core.Security
{
@@ -7,6 +8,12 @@ namespace Umbraco.Cms.Core.Security
/// </summary>
public interface IMemberManager : IUmbracoUserManager<MemberIdentityUser>
{
/// <summary>
/// Returns the currently logged in member if there is one, else returns null
/// </summary>
/// <returns></returns>
Task<MemberIdentityUser> GetCurrentMemberAsync();
/// <summary>
/// Checks if the current member is authorized based on the parameters provided.
/// </summary>
@@ -14,15 +21,38 @@ namespace Umbraco.Cms.Core.Security
/// <param name="allowGroups">Allowed groups.</param>
/// <param name="allowMembers">Allowed individual members.</param>
/// <returns>True or false if the currently logged in member is authorized</returns>
bool IsMemberAuthorized(
Task<bool> IsMemberAuthorizedAsync(
IEnumerable<string> allowTypes = null,
IEnumerable<string> allowGroups = null,
IEnumerable<int> allowMembers = null);
// TODO: We'll need to add some additional things here that people will be using in their code:
/// <summary>
/// Check if a member is logged in
/// </summary>
/// <returns></returns>
bool IsLoggedIn();
// bool MemberHasAccess(string path);
// IReadOnlyDictionary<string, bool> MemberHasAccess(IEnumerable<string> paths)
// Possibly some others from the old MembershipHelper
/// <summary>
/// Check if the current user has access to a document
/// </summary>
/// <param name="path">The full path of the document object to check</param>
/// <returns>True if the current user has access or if the current document isn't protected</returns>
Task<bool> MemberHasAccessAsync(string path);
/// <summary>
/// Checks if the current user has access to the paths
/// </summary>
/// <param name="paths"></param>
/// <returns></returns>
Task<IReadOnlyDictionary<string, bool>> MemberHasAccessAsync(IEnumerable<string> paths);
/// <summary>
/// Check if a document object is protected by the "Protect Pages" functionality in umbraco
/// </summary>
/// <param name="path">The full path of the document object to check</param>
/// <returns>True if the document object is protected</returns>
Task<bool> IsProtectedAsync(string path);
Task<IReadOnlyDictionary<string, bool>> IsProtectedAsync(IEnumerable<string> paths);
}
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Models.Identity;
namespace Umbraco.Cms.Core.Security
{

View File

@@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Cms.Core.Security
{
/// <summary>
/// Identity options specifically for the Umbraco members identity implementation
/// </summary>
public class MemberIdentityOptions : IdentityOptions
{
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Extensions;
@@ -13,9 +12,7 @@ namespace Umbraco.Cms.Core.Security
/// </summary>
public class MemberIdentityUser : UmbracoIdentityUser
{
private string _name;
private string _comments;
private string _passwordConfig;
private string _comments;
private IReadOnlyCollection<IReadOnlyUserGroup> _groups;
// Custom comparer for enumerables
@@ -53,20 +50,11 @@ namespace Umbraco.Cms.Core.Security
user.MemberTypeAlias = memberTypeAlias;
user.Id = null;
user.HasIdentity = false;
user._name = name;
user.Name = name;
user.EnableChangeTracking();
return user;
}
/// <summary>
/// Gets or sets the member's real name
/// </summary>
public string Name
{
get => _name;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
}
/// <summary>
/// Gets or sets the member's comments
/// </summary>
@@ -85,15 +73,6 @@ namespace Umbraco.Cms.Core.Security
// No change tracking because the persisted value is readonly
public Guid Key { get; set; }
/// <summary>
/// Gets or sets the password config
/// </summary>
public string PasswordConfig
{
get => _passwordConfig;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
}
/// <summary>
/// Gets or sets the user groups
/// </summary>
@@ -121,23 +100,6 @@ namespace Umbraco.Cms.Core.Security
}
}
/// <summary>
/// Gets a value indicating whether the member is locked out
/// </summary>
public bool IsLockedOut
{
get
{
bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
return isLocked;
}
}
/// <summary>
/// Gets or sets a value indicating whether the member is approved
/// </summary>
public bool IsApproved { get; set; }
/// <summary>
/// Gets or sets the alias of the member type
/// </summary>

View File

@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.Security
@@ -11,7 +12,7 @@ namespace Umbraco.Cms.Core.Security
/// <summary>
/// A custom user store that uses Umbraco member data
/// </summary>
public class MemberRoleStore : IRoleStore<UmbracoIdentityRole>
public class MemberRoleStore : IRoleStore<UmbracoIdentityRole>, IQueryableRoleStore<UmbracoIdentityRole>
{
private readonly IMemberGroupService _memberGroupService;
private bool _disposed;
@@ -20,7 +21,7 @@ namespace Umbraco.Cms.Core.Security
//TODO: How revealing can the error messages be?
private readonly IdentityError _intParseError = new IdentityError { Code = "IdentityIdParseError", Description = "Cannot parse ID to int" };
private readonly IdentityError _memberGroupNotFoundError = new IdentityError { Code = "IdentityMemberGroupNotFound", Description = "Member group not found" };
private const string genericIdentityErrorCode = "IdentityErrorUserStore";
//private const string genericIdentityErrorCode = "IdentityErrorUserStore";
public MemberRoleStore(IMemberGroupService memberGroupService, IdentityErrorDescriber errorDescriber)
{
@@ -33,6 +34,8 @@ namespace Umbraco.Cms.Core.Security
/// </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)
{
@@ -268,5 +271,6 @@ namespace Umbraco.Cms.Core.Security
throw new ObjectDisposedException(GetType().Name);
}
}
}
}

View File

@@ -9,7 +9,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;

View File

@@ -3,7 +3,7 @@ using System.ComponentModel;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models.Identity
namespace Umbraco.Cms.Core.Security
{
public class UmbracoIdentityRole : IdentityRole, IRememberBeingDirty
{

View File

@@ -6,7 +6,7 @@ using System.ComponentModel;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models.Identity
namespace Umbraco.Cms.Core.Security
{
/// <summary>
@@ -29,6 +29,8 @@ namespace Umbraco.Cms.Core.Models.Identity
/// </remarks>
public abstract class UmbracoIdentityUser : IdentityUser, IRememberBeingDirty
{
private string _name;
private string _passwordConfig;
private string _id;
private string _email;
private string _userName;
@@ -247,6 +249,42 @@ namespace Umbraco.Cms.Core.Models.Identity
/// </summary>
protected BeingDirty BeingDirty { get; } = new BeingDirty();
/// <summary>
/// Gets a value indicating whether the user is locked out based on the user's lockout end date
/// </summary>
public bool IsLockedOut
{
get
{
bool isLocked = LockoutEnabled && LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
return isLocked;
}
}
/// <summary>
/// Gets or sets a value indicating whether the IUser IsApproved
/// </summary>
public bool IsApproved { get; set; }
/// <summary>
/// Gets or sets the user's real name
/// </summary>
public string Name
{
get => _name;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
}
/// <summary>
/// Gets or sets the password config
/// </summary>
public string PasswordConfig
{
// TODO: Implement this for members: AB#11550
get => _passwordConfig;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
}
/// <inheritdoc />
public bool IsDirty() => BeingDirty.IsDirty();

View File

@@ -0,0 +1,15 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Cms.Core.Security
{
/// <summary>
/// Confirms whether a user is approved or not
/// </summary>
public class UmbracoUserConfirmation<TUser> : DefaultUserConfirmation<TUser>
where TUser: UmbracoIdentityUser
{
public override Task<bool> IsConfirmedAsync(UserManager<TUser> manager, TUser user)
=> Task.FromResult(user.IsApproved);
}
}

View File

@@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Net;
namespace Umbraco.Cms.Core.Security

View File

@@ -2,9 +2,9 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Core.Services.Implement
{