Handle sensitive properties in the Management API (#15936)
* Handle sensitive properties in the Management API * Use Assert.Multiple to catch all failing tests in one run --------- Co-authored-by: Sven Geusens <sge@umbraco.dk>
This commit is contained in:
@@ -73,6 +73,10 @@ public abstract class DocumentTypeControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("Duplicate property type alias")
|
||||
.WithDetail("One or more property type aliases are already in use, all property type aliases must be unique.")
|
||||
.Build()),
|
||||
ContentTypeOperationStatus.NotAllowed => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Operation not permitted")
|
||||
.WithDetail("The attempted operation was not permitted, likely due to a permission/configuration mismatch with the operation.")
|
||||
.Build()),
|
||||
_ => new ObjectResult("Unknown content type operation status") { StatusCode = StatusCodes.Status500InternalServerError },
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Member;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Member;
|
||||
@@ -13,11 +14,16 @@ public class ByKeyMemberController : MemberControllerBase
|
||||
{
|
||||
private readonly IMemberEditingService _memberEditingService;
|
||||
private readonly IMemberPresentationFactory _memberPresentationFactory;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public ByKeyMemberController(IMemberEditingService memberEditingService, IMemberPresentationFactory memberPresentationFactory)
|
||||
public ByKeyMemberController(
|
||||
IMemberEditingService memberEditingService,
|
||||
IMemberPresentationFactory memberPresentationFactory,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_memberEditingService = memberEditingService;
|
||||
_memberPresentationFactory = memberPresentationFactory;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
@@ -32,7 +38,7 @@ public class ByKeyMemberController : MemberControllerBase
|
||||
return MemberNotFound();
|
||||
}
|
||||
|
||||
MemberResponseModel model = await _memberPresentationFactory.CreateResponseModelAsync(member);
|
||||
MemberResponseModel model = await _memberPresentationFactory.CreateResponseModelAsync(member, CurrentUser(_backOfficeSecurityAccessor));
|
||||
return Ok(model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Umbraco.Cms.Api.Management.ViewModels.Member;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Member.Filter;
|
||||
@@ -16,13 +17,16 @@ public class FilterMemberFilterController : MemberFilterControllerBase
|
||||
{
|
||||
private readonly IMemberService _memberService;
|
||||
private readonly IMemberPresentationFactory _memberPresentationFactory;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public FilterMemberFilterController(
|
||||
IMemberService memberService,
|
||||
IMemberPresentationFactory memberPresentationFactory)
|
||||
IMemberPresentationFactory memberPresentationFactory,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_memberService = memberService;
|
||||
_memberPresentationFactory = memberPresentationFactory;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -53,7 +57,7 @@ public class FilterMemberFilterController : MemberFilterControllerBase
|
||||
|
||||
var pageViewModel = new PagedViewModel<MemberResponseModel>
|
||||
{
|
||||
Items = await _memberPresentationFactory.CreateMultipleAsync(members.Items),
|
||||
Items = await _memberPresentationFactory.CreateMultipleAsync(members.Items, CurrentUser(_backOfficeSecurityAccessor)),
|
||||
Total = members.Total,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
@@ -13,12 +13,12 @@ namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
public class ByKeyMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
private readonly IMemberTypePresentationFactory _memberTypePresentationFactory;
|
||||
|
||||
public ByKeyMemberTypeController(IMemberTypeService memberTypeService, IUmbracoMapper umbracoMapper)
|
||||
public ByKeyMemberTypeController(IMemberTypeService memberTypeService, IMemberTypePresentationFactory memberTypePresentationFactory)
|
||||
{
|
||||
_memberTypeService = memberTypeService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
_memberTypePresentationFactory = memberTypePresentationFactory;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
@@ -27,13 +27,13 @@ public class ByKeyMemberTypeController : MemberTypeControllerBase
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByKey(Guid id)
|
||||
{
|
||||
IMemberType? MemberType = await _memberTypeService.GetAsync(id);
|
||||
if (MemberType == null)
|
||||
IMemberType? memberType = await _memberTypeService.GetAsync(id);
|
||||
if (memberType is null)
|
||||
{
|
||||
return OperationStatusResult(ContentTypeOperationStatus.NotFound);
|
||||
}
|
||||
|
||||
MemberTypeResponseModel model = _umbracoMapper.Map<MemberTypeResponseModel>(MemberType)!;
|
||||
return await Task.FromResult(Ok(model));
|
||||
MemberTypeResponseModel model = await _memberTypePresentationFactory.CreateResponseModelAsync(memberType);
|
||||
return Ok(model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ internal static class MemberTypeBuilderExtensions
|
||||
{
|
||||
internal static IUmbracoBuilder AddMemberTypes(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddTransient<IMemberTypePresentationFactory, MemberTypePresentationFactory>();
|
||||
builder.Services.AddTransient<IMemberTypeEditingPresentationFactory, MemberTypeEditingPresentationFactory>();
|
||||
|
||||
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<MemberTypeMapDefinition>();
|
||||
|
||||
@@ -4,14 +4,15 @@ using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
public interface IMemberPresentationFactory
|
||||
{
|
||||
Task<MemberResponseModel> CreateResponseModelAsync(IMember member);
|
||||
Task<MemberResponseModel> CreateResponseModelAsync(IMember member, IUser currentUser);
|
||||
|
||||
Task<IEnumerable<MemberResponseModel>> CreateMultipleAsync(IEnumerable<IMember> members);
|
||||
Task<IEnumerable<MemberResponseModel>> CreateMultipleAsync(IEnumerable<IMember> members, IUser currentUser);
|
||||
|
||||
MemberItemResponseModel CreateItemResponseModel(IMemberEntitySlim entity);
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
public interface IMemberTypePresentationFactory
|
||||
{
|
||||
Task<MemberTypeResponseModel> CreateResponseModelAsync(IMemberType memberType);
|
||||
}
|
||||
@@ -2,10 +2,13 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Member;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
using Umbraco.Cms.Core.Extensions;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
@@ -13,34 +16,39 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
|
||||
{
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
private readonly IMemberService _memberService;
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
|
||||
public MemberPresentationFactory(
|
||||
IUmbracoMapper umbracoMapper,
|
||||
IMemberService memberService,
|
||||
IMemberTypeService memberTypeService,
|
||||
ITwoFactorLoginService twoFactorLoginService)
|
||||
{
|
||||
_umbracoMapper = umbracoMapper;
|
||||
_memberService = memberService;
|
||||
_memberTypeService = memberTypeService;
|
||||
_twoFactorLoginService = twoFactorLoginService;
|
||||
}
|
||||
|
||||
public async Task<MemberResponseModel> CreateResponseModelAsync(IMember member)
|
||||
public async Task<MemberResponseModel> CreateResponseModelAsync(IMember member, IUser currentUser)
|
||||
{
|
||||
MemberResponseModel responseModel = _umbracoMapper.Map<MemberResponseModel>(member)!;
|
||||
|
||||
responseModel.IsTwoFactorEnabled = await _twoFactorLoginService.IsTwoFactorEnabledAsync(member.Key);
|
||||
responseModel.Groups = _memberService.GetAllRoles(member.Username);
|
||||
|
||||
return responseModel;
|
||||
return currentUser.HasAccessToSensitiveData()
|
||||
? responseModel
|
||||
: await RemoveSensitiveDataAsync(member, responseModel);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MemberResponseModel>> CreateMultipleAsync(IEnumerable<IMember> members)
|
||||
public async Task<IEnumerable<MemberResponseModel>> CreateMultipleAsync(IEnumerable<IMember> members, IUser currentUser)
|
||||
{
|
||||
var memberResponseModels = new List<MemberResponseModel>();
|
||||
foreach (IMember member in members)
|
||||
{
|
||||
memberResponseModels.Add(await CreateResponseModelAsync(member));
|
||||
memberResponseModels.Add(await CreateResponseModelAsync(member, currentUser));
|
||||
}
|
||||
|
||||
return memberResponseModels;
|
||||
@@ -72,4 +80,29 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
|
||||
|
||||
public MemberTypeReferenceResponseModel CreateMemberTypeReferenceResponseModel(IMemberEntitySlim entity)
|
||||
=> _umbracoMapper.Map<MemberTypeReferenceResponseModel>(entity)!;
|
||||
|
||||
private async Task<MemberResponseModel> RemoveSensitiveDataAsync(IMember member, MemberResponseModel responseModel)
|
||||
{
|
||||
// these properties are considered sensitive; some of them are not nullable, so for
|
||||
// those we can't do much more than force revert them to their default values.
|
||||
responseModel.IsApproved = false;
|
||||
responseModel.IsLockedOut = false;
|
||||
responseModel.IsTwoFactorEnabled = false;
|
||||
responseModel.FailedPasswordAttempts = 0;
|
||||
responseModel.LastLoginDate = null;
|
||||
responseModel.LastLockoutDate = null;
|
||||
responseModel.LastPasswordChangeDate = null;
|
||||
|
||||
IMemberType memberType = await _memberTypeService.GetAsync(member.ContentType.Key)
|
||||
?? throw new InvalidOperationException($"The member type {member.ContentType.Alias} could not be found");
|
||||
|
||||
var sensitivePropertyAliases = memberType.GetSensitivePropertyTypeAliases().ToArray();
|
||||
|
||||
// remove all properties whose property types are flagged as sensitive
|
||||
responseModel.Values = responseModel.Values
|
||||
.Where(valueModel => sensitivePropertyAliases.InvariantContains(valueModel.Alias) is false)
|
||||
.ToArray();
|
||||
|
||||
return responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ internal sealed class MemberTypeEditingPresentationFactory : ContentTypeEditingP
|
||||
createModel.Key = requestModel.Id;
|
||||
createModel.Compositions = MapCompositions(requestModel.Compositions);
|
||||
|
||||
MapPropertyTypeSensitivityAndVisibility(createModel.Properties, requestModel.Properties);
|
||||
|
||||
return createModel;
|
||||
}
|
||||
|
||||
@@ -40,6 +42,8 @@ internal sealed class MemberTypeEditingPresentationFactory : ContentTypeEditingP
|
||||
|
||||
updateModel.Compositions = MapCompositions(requestModel.Compositions);
|
||||
|
||||
MapPropertyTypeSensitivityAndVisibility(updateModel.Properties, requestModel.Properties);
|
||||
|
||||
return updateModel;
|
||||
}
|
||||
|
||||
@@ -50,4 +54,23 @@ internal sealed class MemberTypeEditingPresentationFactory : ContentTypeEditingP
|
||||
=> MapCompositions(documentTypeCompositions
|
||||
.DistinctBy(c => c.MemberType.Id)
|
||||
.ToDictionary(c => c.MemberType.Id, c => c.CompositionType));
|
||||
|
||||
private void MapPropertyTypeSensitivityAndVisibility<TRequestPropertyTypeModel>(
|
||||
IEnumerable<MemberTypePropertyTypeModel> propertyTypes,
|
||||
IEnumerable<TRequestPropertyTypeModel> requestPropertyTypes)
|
||||
where TRequestPropertyTypeModel : MemberTypePropertyTypeModelBase
|
||||
{
|
||||
var requestModelPropertiesByAlias = requestPropertyTypes.ToDictionary(p => p.Alias);
|
||||
foreach (MemberTypePropertyTypeModel propertyType in propertyTypes)
|
||||
{
|
||||
if (requestModelPropertiesByAlias.TryGetValue(propertyType.Alias, out TRequestPropertyTypeModel? requestPropertyType) is false)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not find the property type model {propertyType.Alias} in the request");
|
||||
}
|
||||
|
||||
propertyType.IsSensitive = requestPropertyType.IsSensitive;
|
||||
propertyType.MemberCanView = requestPropertyType.Visibility.MemberCanView;
|
||||
propertyType.MemberCanEdit = requestPropertyType.Visibility.MemberCanEdit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
internal sealed class MemberTypePresentationFactory : IMemberTypePresentationFactory
|
||||
{
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public MemberTypePresentationFactory(IUmbracoMapper umbracoMapper)
|
||||
=> _umbracoMapper = umbracoMapper;
|
||||
|
||||
public Task<MemberTypeResponseModel> CreateResponseModelAsync(IMemberType memberType)
|
||||
{
|
||||
MemberTypeResponseModel model = _umbracoMapper.Map<MemberTypeResponseModel>(memberType)!;
|
||||
|
||||
foreach (MemberTypePropertyTypeResponseModel propertyType in model.Properties)
|
||||
{
|
||||
propertyType.IsSensitive = memberType.IsSensitiveProperty(propertyType.Alias);
|
||||
|
||||
propertyType.Visibility.MemberCanEdit = memberType.MemberCanEditProperty(propertyType.Alias);
|
||||
propertyType.Visibility.MemberCanView = memberType.MemberCanViewProperty(propertyType.Alias);
|
||||
}
|
||||
|
||||
return Task.FromResult(model);
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,7 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
var hasAccessToAllLanguages = presentationGroups.Any(x => x.HasAccessToAllLanguages);
|
||||
|
||||
var allowedSections = presentationGroups.SelectMany(x => x.Sections).ToHashSet();
|
||||
|
||||
return await Task.FromResult(new CurrentUserResponseModel()
|
||||
{
|
||||
Id = presentationUser.Id,
|
||||
@@ -178,7 +179,8 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
Permissions = permissions,
|
||||
FallbackPermissions = fallbackPermissions,
|
||||
HasAccessToAllLanguages = hasAccessToAllLanguages,
|
||||
AllowedSections = allowedSections
|
||||
HasAccessToSensitiveData = user.HasAccessToSensitiveData(),
|
||||
AllowedSections = allowedSections,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -34497,7 +34497,7 @@
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PropertyTypeModelBaseModel"
|
||||
"$ref": "#/components/schemas/MemberTypePropertyTypeModelBaseModel"
|
||||
}
|
||||
],
|
||||
"additionalProperties": false
|
||||
@@ -34739,6 +34739,7 @@
|
||||
"email",
|
||||
"fallbackPermissions",
|
||||
"hasAccessToAllLanguages",
|
||||
"hasAccessToSensitiveData",
|
||||
"id",
|
||||
"languages",
|
||||
"mediaStartNodeIds",
|
||||
@@ -34796,6 +34797,9 @@
|
||||
"hasAccessToAllLanguages": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasAccessToSensitiveData": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"fallbackPermissions": {
|
||||
"uniqueItems": true,
|
||||
"type": "array",
|
||||
@@ -37652,15 +37656,115 @@
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MemberTypePropertyTypeModelBaseModel": {
|
||||
"required": [
|
||||
"alias",
|
||||
"appearance",
|
||||
"dataType",
|
||||
"id",
|
||||
"isSensitive",
|
||||
"name",
|
||||
"sortOrder",
|
||||
"validation",
|
||||
"variesByCulture",
|
||||
"variesBySegment",
|
||||
"visibility"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"container": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ReferenceByIdModel"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"sortOrder": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"alias": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"dataType": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ReferenceByIdModel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variesByCulture": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"variesBySegment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"validation": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PropertyTypeValidationModel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PropertyTypeAppearanceModel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"isSensitive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"visibility": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/MemberTypePropertyTypeVisibilityModel"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MemberTypePropertyTypeResponseModel": {
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PropertyTypeModelBaseModel"
|
||||
"$ref": "#/components/schemas/MemberTypePropertyTypeModelBaseModel"
|
||||
}
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MemberTypePropertyTypeVisibilityModel": {
|
||||
"required": [
|
||||
"memberCanEdit",
|
||||
"memberCanView"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memberCanView": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"memberCanEdit": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MemberTypeReferenceResponseModel": {
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
@@ -41472,7 +41576,7 @@
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PropertyTypeModelBaseModel"
|
||||
"$ref": "#/components/schemas/MemberTypePropertyTypeModelBaseModel"
|
||||
}
|
||||
],
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
|
||||
public class CreateMemberTypePropertyTypeRequestModel : PropertyTypeModelBase
|
||||
public class CreateMemberTypePropertyTypeRequestModel : MemberTypePropertyTypeModelBase
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
|
||||
public abstract class MemberTypePropertyTypeModelBase : PropertyTypeModelBase
|
||||
{
|
||||
public bool IsSensitive { get; set; }
|
||||
|
||||
public MemberTypePropertyTypeVisibility Visibility { get; set; } = new();
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
|
||||
public class MemberTypePropertyTypeResponseModel : PropertyTypeModelBase
|
||||
public class MemberTypePropertyTypeResponseModel : MemberTypePropertyTypeModelBase
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
|
||||
public class MemberTypePropertyTypeVisibility
|
||||
{
|
||||
public bool MemberCanView { get; set; }
|
||||
|
||||
public bool MemberCanEdit { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
|
||||
public class UpdateMemberTypePropertyTypeRequestModel : PropertyTypeModelBase
|
||||
public class UpdateMemberTypePropertyTypeRequestModel : MemberTypePropertyTypeModelBase
|
||||
{
|
||||
}
|
||||
|
||||
@@ -26,7 +26,11 @@ public class CurrentUserResponseModel
|
||||
|
||||
public required bool HasAccessToAllLanguages { get; init; }
|
||||
|
||||
public required bool HasAccessToSensitiveData { get; set; }
|
||||
|
||||
public required ISet<string> FallbackPermissions { get; init; }
|
||||
|
||||
public required ISet<IPermissionPresentationModel> Permissions { get; init; }
|
||||
|
||||
public required ISet<string> AllowedSections { get; init; }
|
||||
}
|
||||
|
||||
12
src/Umbraco.Core/Extensions/MemberTypeExtensions.cs
Normal file
12
src/Umbraco.Core/Extensions/MemberTypeExtensions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Extensions;
|
||||
|
||||
public static class MemberTypeExtensions
|
||||
{
|
||||
public static IEnumerable<IPropertyType> GetSensitivePropertyTypes(this IMemberType memberType)
|
||||
=> memberType.CompositionPropertyTypes.Where(p => memberType.IsSensitiveProperty(p.Alias));
|
||||
|
||||
public static IEnumerable<string> GetSensitivePropertyTypeAliases(this IMemberType memberType)
|
||||
=> memberType.GetSensitivePropertyTypes().Select(p => p.Alias);
|
||||
}
|
||||
@@ -2,4 +2,9 @@
|
||||
|
||||
public class MemberTypePropertyTypeModel : PropertyTypeModelBase
|
||||
{
|
||||
public bool IsSensitive { get; set; }
|
||||
|
||||
public bool MemberCanView { get; set; }
|
||||
|
||||
public bool MemberCanEdit { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
|
||||
@@ -8,37 +9,58 @@ namespace Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
internal sealed class MemberTypeEditingService : ContentTypeEditingServiceBase<IMemberType, IMemberTypeService, MemberTypePropertyTypeModel, MemberTypePropertyContainerModel>, IMemberTypeEditingService
|
||||
{
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public MemberTypeEditingService(
|
||||
IContentTypeService contentTypeService,
|
||||
IMemberTypeService memberTypeService,
|
||||
IDataTypeService dataTypeService,
|
||||
IEntityService entityService,
|
||||
IShortStringHelper shortStringHelper)
|
||||
IShortStringHelper shortStringHelper,
|
||||
IUserService userService)
|
||||
: base(contentTypeService, memberTypeService, dataTypeService, entityService, shortStringHelper)
|
||||
=> _memberTypeService = memberTypeService;
|
||||
{
|
||||
_memberTypeService = memberTypeService;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
public async Task<Attempt<IMemberType?, ContentTypeOperationStatus>> CreateAsync(MemberTypeCreateModel model, Guid userKey)
|
||||
{
|
||||
Attempt<IMemberType?, ContentTypeOperationStatus> result = await ValidateAndMapForCreationAsync(model, model.Key, containerKey: null);
|
||||
if (result.Success)
|
||||
if (result.Success is false)
|
||||
{
|
||||
IMemberType memberType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForCreationAsync)} succeeded but did not yield any result");
|
||||
await _memberTypeService.SaveAsync(memberType, userKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
IMemberType memberType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForCreationAsync)} succeeded but did not yield any result");
|
||||
if (await UpdatePropertyTypeSensitivity(memberType, model, userKey) is false)
|
||||
{
|
||||
return Attempt.FailWithStatus<IMemberType?, ContentTypeOperationStatus>(ContentTypeOperationStatus.NotAllowed, memberType);
|
||||
}
|
||||
|
||||
UpdatePropertyTypeVisibility(memberType, model);
|
||||
|
||||
await _memberTypeService.SaveAsync(memberType, userKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Attempt<IMemberType?, ContentTypeOperationStatus>> UpdateAsync(IMemberType memberType, MemberTypeUpdateModel model, Guid userKey)
|
||||
{
|
||||
Attempt<IMemberType?, ContentTypeOperationStatus> result = await ValidateAndMapForUpdateAsync(memberType, model);
|
||||
if (result.Success)
|
||||
if (result.Success is false)
|
||||
{
|
||||
memberType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result");
|
||||
await _memberTypeService.SaveAsync(memberType, userKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
memberType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result");
|
||||
if (await UpdatePropertyTypeSensitivity(memberType, model, userKey) is false)
|
||||
{
|
||||
return Attempt.FailWithStatus<IMemberType?, ContentTypeOperationStatus>(ContentTypeOperationStatus.NotAllowed, memberType);
|
||||
}
|
||||
|
||||
UpdatePropertyTypeVisibility(memberType, model);
|
||||
|
||||
await _memberTypeService.SaveAsync(memberType, userKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -56,4 +78,34 @@ internal sealed class MemberTypeEditingService : ContentTypeEditingServiceBase<I
|
||||
protected override UmbracoObjectTypes ContentTypeObjectType => UmbracoObjectTypes.MemberType;
|
||||
|
||||
protected override UmbracoObjectTypes ContainerObjectType => throw new NotSupportedException("Member type tree does not support containers");
|
||||
|
||||
private void UpdatePropertyTypeVisibility(IMemberType memberType, MemberTypeModelBase model)
|
||||
{
|
||||
foreach (MemberTypePropertyTypeModel propertyType in model.Properties)
|
||||
{
|
||||
memberType.SetMemberCanViewProperty(propertyType.Alias, propertyType.MemberCanView);
|
||||
memberType.SetMemberCanEditProperty(propertyType.Alias, propertyType.MemberCanEdit);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> UpdatePropertyTypeSensitivity(IMemberType memberType, MemberTypeModelBase model, Guid userKey)
|
||||
{
|
||||
IUser user = await _userService.GetAsync(userKey)
|
||||
?? throw new ArgumentException($"Could not find a user with the specified user key ({userKey})", nameof(userKey));
|
||||
|
||||
var canChangeSensitivity = user.HasAccessToSensitiveData();
|
||||
|
||||
foreach (MemberTypePropertyTypeModel propertyType in model.Properties)
|
||||
{
|
||||
var changed = memberType.IsSensitiveProperty(propertyType.Alias) != propertyType.IsSensitive;
|
||||
if (changed && canChangeSensitivity is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
memberType.SetIsSensitiveProperty(propertyType.Alias, propertyType.IsSensitive);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Extensions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
@@ -11,6 +13,7 @@ internal sealed class MemberContentEditingService
|
||||
: ContentEditingServiceBase<IMember, IMemberType, IMemberService, IMemberTypeService>, IMemberContentEditingService
|
||||
{
|
||||
private readonly ILogger<ContentEditingServiceBase<IMember, IMemberType, IMemberService, IMemberTypeService>> _logger;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public MemberContentEditingService(
|
||||
IMemberService contentService,
|
||||
@@ -20,22 +23,42 @@ internal sealed class MemberContentEditingService
|
||||
ILogger<ContentEditingServiceBase<IMember, IMemberType, IMemberService, IMemberTypeService>> logger,
|
||||
ICoreScopeProvider scopeProvider,
|
||||
IUserIdKeyResolver userIdKeyResolver,
|
||||
IMemberValidationService memberValidationService)
|
||||
IMemberValidationService memberValidationService,
|
||||
IUserService userService)
|
||||
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, memberValidationService)
|
||||
=> _logger = logger;
|
||||
{
|
||||
_logger = logger;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidateAsync(MemberEditingModelBase editingModel, Guid memberTypeKey)
|
||||
=> await ValidatePropertiesAsync(editingModel, memberTypeKey);
|
||||
|
||||
public async Task<Attempt<MemberUpdateResult, ContentEditingOperationStatus>> UpdateAsync(IMember member, MemberEditingModelBase updateModel, Guid userKey)
|
||||
{
|
||||
// FIXME: handle sensitive property data
|
||||
IMemberType memberType = await ContentTypeService.GetAsync(member.ContentType.Key)
|
||||
?? throw new InvalidOperationException($"The member type {member.ContentType.Alias} could not be found.");
|
||||
|
||||
IUser user = await _userService.GetAsync(userKey)
|
||||
?? throw new InvalidOperationException("The supplied user key did not match any existing user");
|
||||
|
||||
if (ValidateAccessToSensitiveProperties(member, memberType, updateModel, user) is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(ContentEditingOperationStatus.NotAllowed, new MemberUpdateResult());
|
||||
}
|
||||
|
||||
// store any sensitive properties that must be explicitly carried over between updates (due to missing user access to sensitive data)
|
||||
Dictionary<string, object?> sensitivePropertiesToRetain = FindSensitivePropertiesToRetain(member, memberType, user);
|
||||
|
||||
Attempt<MemberUpdateResult, ContentEditingOperationStatus> result = await MapUpdate<MemberUpdateResult>(member, updateModel);
|
||||
if (result.Success == false)
|
||||
{
|
||||
return Attempt.FailWithStatus(result.Status, result.Result);
|
||||
}
|
||||
|
||||
// restore the retained sensitive properties after the base update (they have been removed from the member at this point)
|
||||
RetainSensitiveProperties(member, sensitivePropertiesToRetain);
|
||||
|
||||
// the create mapping might succeed, but this doesn't mean the model is valid at property level.
|
||||
// we'll return the actual property validation status if the entire operation succeeds.
|
||||
ContentEditingOperationStatus validationStatus = result.Status;
|
||||
@@ -51,7 +74,6 @@ internal sealed class MemberContentEditingService
|
||||
public async Task<Attempt<IMember?, ContentEditingOperationStatus>> DeleteAsync(Guid key, Guid userKey)
|
||||
=> await HandleDeleteAsync(key, userKey, false);
|
||||
|
||||
|
||||
protected override IMember New(string? name, int parentId, IMemberType memberType)
|
||||
=> throw new NotSupportedException("Member creation is not supported by this service. This should never be called.");
|
||||
|
||||
@@ -88,4 +110,41 @@ internal sealed class MemberContentEditingService
|
||||
return ContentEditingOperationStatus.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateAccessToSensitiveProperties(IMember member, IMemberType memberType, MemberEditingModelBase updateModel, IUser user)
|
||||
{
|
||||
if (user.HasAccessToSensitiveData())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var sensitivePropertyAliases = memberType.GetSensitivePropertyTypeAliases().ToArray();
|
||||
return updateModel
|
||||
.InvariantProperties
|
||||
.Union(updateModel.Variants.SelectMany(variant => variant.Properties))
|
||||
.Select(property => property.Alias)
|
||||
.Intersect(sensitivePropertyAliases, StringComparer.OrdinalIgnoreCase)
|
||||
.Any() is false;
|
||||
}
|
||||
|
||||
private Dictionary<string, object?> FindSensitivePropertiesToRetain(IMember member, IMemberType memberType, IUser user)
|
||||
{
|
||||
if (user.HasAccessToSensitiveData())
|
||||
{
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
var sensitivePropertyAliases = memberType.GetSensitivePropertyTypeAliases().ToArray();
|
||||
// NOTE: this is explicitly NOT handling variance. if variance becomes a thing for members, this needs to be amended.
|
||||
return sensitivePropertyAliases.ToDictionary(alias => alias, alias => member.GetValue(alias));
|
||||
}
|
||||
|
||||
private void RetainSensitiveProperties(IMember member, Dictionary<string, object?> sensitivePropertyValues)
|
||||
{
|
||||
foreach (KeyValuePair<string, object?> sensitiveProperty in sensitivePropertyValues)
|
||||
{
|
||||
// NOTE: this is explicitly NOT handling variance. if variance becomes a thing for members, this needs to be amended.
|
||||
member.SetValue(sensitiveProperty.Key, sensitiveProperty.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ public enum ContentTypeOperationStatus
|
||||
MissingContainer,
|
||||
DuplicateContainer,
|
||||
NotFound,
|
||||
NotAllowed
|
||||
}
|
||||
|
||||
@@ -111,7 +111,17 @@ internal sealed class MemberEditingService : IMemberEditingService
|
||||
if (member is null)
|
||||
{
|
||||
status.ContentEditingOperationStatus = ContentEditingOperationStatus.NotFound;
|
||||
return Attempt.FailWithStatus(new MemberEditingStatus(), new MemberUpdateResult());
|
||||
return Attempt.FailWithStatus(status, new MemberUpdateResult());
|
||||
}
|
||||
|
||||
if (user.HasAccessToSensitiveData() is false)
|
||||
{
|
||||
// handle sensitive data. certain member properties (IsApproved, IsLockedOut) are subject to "sensitive data" rules.
|
||||
if (member.IsLockedOut != updateModel.IsLockedOut || member.IsApproved != updateModel.IsApproved)
|
||||
{
|
||||
status.ContentEditingOperationStatus = ContentEditingOperationStatus.NotAllowed;
|
||||
return Attempt.FailWithStatus(status, new MemberUpdateResult());
|
||||
}
|
||||
}
|
||||
|
||||
MemberIdentityUser? identityMember = await _memberManager.FindByIdAsync(member.Id.ToString());
|
||||
@@ -165,8 +175,6 @@ internal sealed class MemberEditingService : IMemberEditingService
|
||||
return Attempt.FailWithStatus(status, new MemberUpdateResult { Content = member });
|
||||
}
|
||||
|
||||
// FIXME: handle sensitive data. certain properties (IsApproved, IsLockedOut, ...) are subject to "sensitive data" rules.
|
||||
// reverse engineer what's happening in the old backoffice MemberController and replicate here
|
||||
member.IsLockedOut = updateModel.IsLockedOut;
|
||||
member.IsApproved = updateModel.IsApproved;
|
||||
member.Email = updateModel.Email;
|
||||
|
||||
@@ -131,7 +131,7 @@ public abstract class ContentTypeEditingServiceTestsBase : UmbracoIntegrationTes
|
||||
Guid? key = null)
|
||||
=> CreateContainer<MediaTypePropertyContainerModel>(name, type, key);
|
||||
|
||||
private TModel CreateContentEditingModel<TModel, TPropertyType, TPropertyTypeContainer>(
|
||||
protected TModel CreateContentEditingModel<TModel, TPropertyType, TPropertyTypeContainer>(
|
||||
string name,
|
||||
string? alias = null,
|
||||
bool isElement = false,
|
||||
@@ -151,7 +151,7 @@ public abstract class ContentTypeEditingServiceTestsBase : UmbracoIntegrationTes
|
||||
IsElement = isElement
|
||||
};
|
||||
|
||||
private TModel CreatePropertyType<TModel>(
|
||||
protected TModel CreatePropertyType<TModel>(
|
||||
string name,
|
||||
string? alias = null,
|
||||
Guid? key = null,
|
||||
@@ -169,7 +169,7 @@ public abstract class ContentTypeEditingServiceTestsBase : UmbracoIntegrationTes
|
||||
Appearance = new PropertyTypeAppearance(),
|
||||
};
|
||||
|
||||
private TModel CreateContainer<TModel>(
|
||||
protected TModel CreateContainer<TModel>(
|
||||
string name,
|
||||
string type = TabContainerType,
|
||||
Guid? key = null)
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the member type editing service. Please notice that a lot of functional test is covered by the content type
|
||||
/// editing service tests, since these services share the same base implementation.
|
||||
/// </summary>
|
||||
public class MemberTypeEditingServiceTests : ContentTypeEditingServiceTestsBase
|
||||
{
|
||||
private IMemberTypeEditingService MemberTypeEditingService => GetRequiredService<IMemberTypeEditingService>();
|
||||
|
||||
private IMemberTypeService MemberTypeService => GetRequiredService<IMemberTypeService>();
|
||||
|
||||
private IUserService UserService => GetRequiredService<IUserService>();
|
||||
|
||||
[Test]
|
||||
public async Task Can_Create_Sensitive_Properties_With_Sensitive_Data_Access()
|
||||
{
|
||||
// arrange
|
||||
var createModel = MemberTypeCreateModel(propertyTypes: new[] { MemberTypePropertyTypeModel(isSensitive: true) });
|
||||
|
||||
// act
|
||||
var result = await MemberTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// assert
|
||||
var memberType = await MemberTypeService.GetAsync(result.Result.Key)!;
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.IsNotEmpty(memberType.PropertyTypes);
|
||||
Assert.IsTrue(memberType.PropertyTypes.All(propertyType => memberType.IsSensitiveProperty(propertyType.Alias)));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_Change_Property_Sensitivity_With_Sensitive_Data_Access(bool initialIsSensitiveValue)
|
||||
{
|
||||
// arrange
|
||||
var createModel = MemberTypeCreateModel(propertyTypes: new[] { MemberTypePropertyTypeModel(isSensitive: initialIsSensitiveValue) });
|
||||
IMemberType memberType = (await MemberTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!;
|
||||
|
||||
var newIsSensitiveValue = initialIsSensitiveValue is false;
|
||||
var updateModel = MemberTypeUpdateModel(propertyTypes: new[] { MemberTypePropertyTypeModel(isSensitive: newIsSensitiveValue) });
|
||||
|
||||
// act
|
||||
var result = await MemberTypeEditingService.UpdateAsync(memberType, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// assert
|
||||
memberType = await MemberTypeService.GetAsync(memberType.Key)!;
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentTypeOperationStatus.Success, result.Status);
|
||||
Assert.IsTrue(memberType.PropertyTypes.All(propertyType => memberType.IsSensitiveProperty(propertyType.Alias) == newIsSensitiveValue));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Create_Sensitive_Properties_Without_Sensitive_Data_Access()
|
||||
{
|
||||
// arrange
|
||||
// this user does NOT have access to sensitive data
|
||||
var user = UserBuilder.CreateUser();
|
||||
UserService.Save(user);
|
||||
var createModel = MemberTypeCreateModel(propertyTypes: new[] { MemberTypePropertyTypeModel(isSensitive: true) });
|
||||
|
||||
// act
|
||||
var result = await MemberTypeEditingService.CreateAsync(createModel, user.Key);
|
||||
|
||||
// assert
|
||||
var memberType = await MemberTypeService.GetAsync(result.Result.Key)!;
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsNull(memberType);
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Cannot_Change_Property_Sensitivity_Without_Sensitive_Data_Access(bool initialIsSensitiveValue)
|
||||
{
|
||||
// arrange
|
||||
// this user does NOT have access to sensitive data
|
||||
var user = UserBuilder.CreateUser();
|
||||
UserService.Save(user);
|
||||
|
||||
var createModel = MemberTypeCreateModel(propertyTypes: new[] { MemberTypePropertyTypeModel(isSensitive: initialIsSensitiveValue) });
|
||||
IMemberType memberType = (await MemberTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!;
|
||||
|
||||
var newIsSensitiveValue = initialIsSensitiveValue is false;
|
||||
var updateModel = MemberTypeUpdateModel(propertyTypes: new[] { MemberTypePropertyTypeModel(isSensitive: newIsSensitiveValue) });
|
||||
|
||||
// act
|
||||
var result = await MemberTypeEditingService.UpdateAsync(memberType, updateModel, user.Key);
|
||||
|
||||
// assert
|
||||
memberType = await MemberTypeService.GetAsync(memberType.Key)!;
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(ContentTypeOperationStatus.NotAllowed, result.Status);
|
||||
Assert.IsTrue(memberType.PropertyTypes.All(propertyType => memberType.IsSensitiveProperty(propertyType.Alias) == initialIsSensitiveValue));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
[TestCase(true, true)]
|
||||
[TestCase(true, false)]
|
||||
[TestCase(false, true)]
|
||||
[TestCase(false, false)]
|
||||
public async Task Can_Define_Property_Visibility_When_Creating(bool memberCanView, bool memberCanEdit)
|
||||
{
|
||||
// arrange
|
||||
var createModel = MemberTypeCreateModel(propertyTypes: new[] { MemberTypePropertyTypeModel(memberCanView: memberCanView, memberCanEdit: memberCanEdit) });
|
||||
|
||||
// act
|
||||
var result = await MemberTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// assert
|
||||
var memberType = await MemberTypeService.GetAsync(result.Result.Key)!;
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.IsNotEmpty(memberType.PropertyTypes);
|
||||
Assert.IsTrue(memberType.PropertyTypes.All(propertyType => memberType.MemberCanViewProperty(propertyType.Alias) == memberCanView));
|
||||
Assert.IsTrue(memberType.PropertyTypes.All(propertyType => memberType.MemberCanEditProperty(propertyType.Alias) == memberCanEdit));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(true, true)]
|
||||
[TestCase(true, false)]
|
||||
[TestCase(false, true)]
|
||||
[TestCase(false, false)]
|
||||
public async Task Can_Update_Property_Visibility(bool memberCanView, bool memberCanEdit)
|
||||
{
|
||||
// arrange
|
||||
var createModel = MemberTypeCreateModel(propertyTypes: new[] { MemberTypePropertyTypeModel(memberCanView: memberCanView is false, memberCanEdit: memberCanEdit is false) });
|
||||
IMemberType memberType = (await MemberTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!;
|
||||
var updateModel = MemberTypeUpdateModel(propertyTypes: new[] { MemberTypePropertyTypeModel(memberCanView: memberCanView, memberCanEdit: memberCanEdit) });
|
||||
|
||||
// act
|
||||
var result = await MemberTypeEditingService.UpdateAsync(memberType, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
// assert
|
||||
memberType = await MemberTypeService.GetAsync(result.Result.Key)!;
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsNotEmpty(memberType.PropertyTypes);
|
||||
Assert.IsTrue(memberType.PropertyTypes.All(propertyType => memberType.MemberCanViewProperty(propertyType.Alias) == memberCanView));
|
||||
Assert.IsTrue(memberType.PropertyTypes.All(propertyType => memberType.MemberCanEditProperty(propertyType.Alias) == memberCanEdit));
|
||||
});
|
||||
}
|
||||
|
||||
private MemberTypeCreateModel MemberTypeCreateModel(
|
||||
string name = "Test",
|
||||
string? alias = null,
|
||||
Guid? key = null,
|
||||
IEnumerable<MemberTypePropertyTypeModel>? propertyTypes = null)
|
||||
{
|
||||
var model = CreateContentEditingModel<MemberTypeCreateModel, MemberTypePropertyTypeModel, MemberTypePropertyContainerModel>(
|
||||
name,
|
||||
alias,
|
||||
isElement: false,
|
||||
propertyTypes);
|
||||
model.Key = key ?? Guid.NewGuid();
|
||||
model.Alias = alias ?? ShortStringHelper.CleanStringForSafeAlias(name);
|
||||
return model;
|
||||
}
|
||||
|
||||
private MemberTypeUpdateModel MemberTypeUpdateModel(
|
||||
string name = "Test",
|
||||
string? alias = null,
|
||||
IEnumerable<MemberTypePropertyTypeModel>? propertyTypes = null)
|
||||
=> CreateContentEditingModel<MemberTypeUpdateModel, MemberTypePropertyTypeModel, MemberTypePropertyContainerModel>(
|
||||
name,
|
||||
alias,
|
||||
isElement: false,
|
||||
propertyTypes);
|
||||
|
||||
private MemberTypePropertyTypeModel MemberTypePropertyTypeModel(
|
||||
string name = "Title",
|
||||
string? alias = null,
|
||||
Guid? key = null,
|
||||
Guid? dataTypeKey = null,
|
||||
bool isSensitive = false,
|
||||
bool memberCanView = false,
|
||||
bool memberCanEdit = false)
|
||||
{
|
||||
var propertyType = CreatePropertyType<MemberTypePropertyTypeModel>(name, alias, key, containerKey: null, dataTypeKey);
|
||||
propertyType.IsSensitive = isSensitive;
|
||||
propertyType.MemberCanView = memberCanView;
|
||||
propertyType.MemberCanEdit = memberCanEdit;
|
||||
return propertyType;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@ public class MemberEditingServiceTests : UmbracoIntegrationTest
|
||||
|
||||
private IMemberTypeService MemberTypeService => GetRequiredService<IMemberTypeService>();
|
||||
|
||||
private IUserService UserService => GetRequiredService<IUserService>();
|
||||
|
||||
[Test]
|
||||
public async Task Can_Create_Member()
|
||||
{
|
||||
@@ -263,11 +265,171 @@ public class MemberEditingServiceTests : UmbracoIntegrationTest
|
||||
Assert.AreEqual(authorValue, result.Result.Content!.GetValue<string>("author"));
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Cannot_Update_Sensitive_Properties_Without_Access(bool useSuperUser)
|
||||
{
|
||||
// this user does NOT have access to sensitive data
|
||||
var user = UserBuilder.CreateUser();
|
||||
UserService.Save(user);
|
||||
|
||||
var member = await CreateMemberAsync(titleIsSensitive: true);
|
||||
|
||||
var updateModel = new MemberUpdateModel
|
||||
{
|
||||
Email = "test-updated@test.com",
|
||||
Username = "test-updated",
|
||||
IsApproved = member.IsApproved,
|
||||
InvariantName = "T. Est Updated",
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = "title", Value = "The updated title value" },
|
||||
new PropertyValueModel { Alias = "author", Value = "The updated author value" }
|
||||
}
|
||||
};
|
||||
|
||||
var result = await MemberEditingService.UpdateAsync(member.Key, updateModel, useSuperUser ? SuperUser() : user);
|
||||
if (useSuperUser)
|
||||
{
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status.ContentEditingOperationStatus);
|
||||
Assert.AreEqual(MemberEditingOperationStatus.Success, result.Status.MemberEditingOperationStatus);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.NotAllowed, result.Status.ContentEditingOperationStatus);
|
||||
Assert.AreEqual(MemberEditingOperationStatus.Success, result.Status.MemberEditingOperationStatus);
|
||||
}
|
||||
|
||||
member = await MemberEditingService.GetAsync(member.Key);
|
||||
Assert.IsNotNull(member);
|
||||
|
||||
if (useSuperUser)
|
||||
{
|
||||
Assert.AreEqual("The updated title value", member.GetValue<string>("title"));
|
||||
Assert.AreEqual("The updated author value", member.GetValue<string>("author"));
|
||||
|
||||
Assert.AreEqual("test-updated@test.com", member.Email);
|
||||
Assert.AreEqual("test-updated", member.Username);
|
||||
Assert.AreEqual("T. Est Updated", member.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.AreEqual("The title value", member.GetValue<string>("title"));
|
||||
Assert.AreEqual("The author value", member.GetValue<string>("author"));
|
||||
|
||||
Assert.AreEqual("test@test.com", member.Email);
|
||||
Assert.AreEqual("test", member.Username);
|
||||
Assert.AreEqual("T. Est", member.Name);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Sensitive_Properties_Are_Retained_When_Updating_Without_Access()
|
||||
{
|
||||
// this user does NOT have access to sensitive data
|
||||
var user = UserBuilder.CreateUser();
|
||||
UserService.Save(user);
|
||||
|
||||
var member = await CreateMemberAsync(titleIsSensitive: true);
|
||||
|
||||
var updateModel = new MemberUpdateModel
|
||||
{
|
||||
Email = "test-updated@test.com",
|
||||
Username = "test-updated",
|
||||
IsApproved = member.IsApproved,
|
||||
InvariantName = "T. Est Updated",
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = "author", Value = "The updated author value" }
|
||||
}
|
||||
};
|
||||
|
||||
var result = await MemberEditingService.UpdateAsync(member.Key, updateModel, user);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status.ContentEditingOperationStatus);
|
||||
Assert.AreEqual(MemberEditingOperationStatus.Success, result.Status.MemberEditingOperationStatus);
|
||||
|
||||
member = await MemberEditingService.GetAsync(member.Key);
|
||||
Assert.IsNotNull(member);
|
||||
|
||||
Assert.AreEqual("The title value", member.GetValue<string>("title"));
|
||||
Assert.AreEqual("The updated author value", member.GetValue<string>("author"));
|
||||
|
||||
Assert.AreEqual("test-updated@test.com", member.Email);
|
||||
Assert.AreEqual("test-updated", member.Username);
|
||||
Assert.AreEqual("T. Est Updated", member.Name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Change_IsApproved_Without_Access()
|
||||
{
|
||||
// this user does NOT have access to sensitive data
|
||||
var user = UserBuilder.CreateUser();
|
||||
UserService.Save(user);
|
||||
|
||||
var member = await CreateMemberAsync();
|
||||
|
||||
var updateModel = new MemberUpdateModel
|
||||
{
|
||||
Email = member.Email,
|
||||
Username = member.Username,
|
||||
IsApproved = false,
|
||||
InvariantName = member.Name,
|
||||
InvariantProperties = member.Properties.Select(property => new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias,
|
||||
Value = property.GetValue()
|
||||
})
|
||||
};
|
||||
|
||||
var result = await MemberEditingService.UpdateAsync(member.Key, updateModel, user);
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.NotAllowed, result.Status.ContentEditingOperationStatus);
|
||||
|
||||
member = await MemberEditingService.GetAsync(member.Key);
|
||||
Assert.IsNotNull(member);
|
||||
Assert.IsTrue(member.IsApproved);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Change_IsLockedOut_Without_Access()
|
||||
{
|
||||
// this user does NOT have access to sensitive data
|
||||
var user = UserBuilder.CreateUser();
|
||||
UserService.Save(user);
|
||||
|
||||
var member = await CreateMemberAsync();
|
||||
|
||||
var updateModel = new MemberUpdateModel
|
||||
{
|
||||
Email = member.Email,
|
||||
Username = member.Username,
|
||||
IsLockedOut = true,
|
||||
InvariantName = member.Name,
|
||||
InvariantProperties = member.Properties.Select(property => new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias,
|
||||
Value = property.GetValue()
|
||||
})
|
||||
};
|
||||
|
||||
var result = await MemberEditingService.UpdateAsync(member.Key, updateModel, user);
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.NotAllowed, result.Status.ContentEditingOperationStatus);
|
||||
|
||||
member = await MemberEditingService.GetAsync(member.Key);
|
||||
Assert.IsNotNull(member);
|
||||
Assert.IsFalse(member.IsLockedOut);
|
||||
}
|
||||
|
||||
private IUser SuperUser() => GetRequiredService<IUserService>().GetAsync(Constants.Security.SuperUserKey).GetAwaiter().GetResult();
|
||||
|
||||
private async Task<IMember> CreateMemberAsync(Guid? key = null)
|
||||
private async Task<IMember> CreateMemberAsync(Guid? key = null, bool titleIsSensitive = false)
|
||||
{
|
||||
IMemberType memberType = MemberTypeBuilder.CreateSimpleMemberType();
|
||||
memberType.SetIsSensitiveProperty("title", titleIsSensitive);
|
||||
MemberTypeService.Save(memberType);
|
||||
MemberService.AddRole("RoleOne");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user