Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9505011d71 | ||
|
|
2579aaf2db | ||
|
|
2b7784a226 | ||
|
|
8555a97b39 | ||
|
|
c2dd685a4b | ||
|
|
66fc819379 | ||
|
|
4f1f7e15c4 | ||
|
|
a826c52e2e | ||
|
|
8b2c22aaf1 | ||
|
|
aecfee4469 | ||
|
|
9c785a9c5b | ||
|
|
2fe10387ee |
@@ -4,13 +4,11 @@ pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily midnight build
|
||||
- cron: '0 6 * * *'
|
||||
displayName: Daily 6 AM build (v16/dev)
|
||||
branches:
|
||||
include:
|
||||
- v15/dev
|
||||
- v16/dev
|
||||
- main
|
||||
|
||||
parameters:
|
||||
- name: skipIntegrationTests
|
||||
@@ -295,7 +293,8 @@ stages:
|
||||
|
||||
- stage: DefaultConfigE2E
|
||||
displayName: Default Config E2E Tests
|
||||
dependsOn: Build
|
||||
dependsOn: Integration
|
||||
condition: always()
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
|
||||
# Enable console logging in Release mode
|
||||
@@ -476,7 +475,8 @@ stages:
|
||||
|
||||
- stage: AdditionalConfigE2E
|
||||
displayName: Additional Config E2E Tests
|
||||
dependsOn: Build
|
||||
dependsOn: DefaultConfigE2E
|
||||
condition: always()
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
|
||||
ASPNETCORE_URLS: https://localhost:44331
|
||||
|
||||
@@ -1,31 +1,60 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DomainsController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IDomainService _domainService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public DomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public DomainsController(IAuthorizationService authorizationService, IDomainService domainService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_domainService = domainService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public DomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
domainService,
|
||||
umbracoMapper)
|
||||
{
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet("{id:guid}/domains")]
|
||||
[ProducesResponseType(typeof(DomainsResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Domains(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IDomain[] assignedDomains = (await _domainService.GetAssignedDomainsAsync(id, true))
|
||||
.OrderBy(d => d.SortOrder)
|
||||
.ToArray();
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
@@ -15,17 +21,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
[ApiVersion("1.0")]
|
||||
public class UpdateDomainsController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IDomainService _domainService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
private readonly IDomainPresentationFactory _domainPresentationFactory;
|
||||
|
||||
public UpdateDomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UpdateDomainsController(IAuthorizationService authorizationService, IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_domainService = domainService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
_domainPresentationFactory = domainPresentationFactory;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public UpdateDomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
domainService,
|
||||
umbracoMapper,
|
||||
domainPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpPut("{id:guid}/domains")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
@@ -37,6 +56,16 @@ public class UpdateDomainsController : DocumentControllerBase
|
||||
Guid id,
|
||||
UpdateDomainsRequestModel updateModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionAssignDomain.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
DomainsUpdateModel domainsUpdateModel = _umbracoMapper.Map<DomainsUpdateModel>(updateModel)!;
|
||||
|
||||
Attempt<DomainUpdateResult, DomainOperationStatus> result = await _domainService.UpdateDomainsAsync(id, domainsUpdateModel);
|
||||
|
||||
+31
-1
@@ -1,33 +1,63 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class UpdateNotificationsController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public UpdateNotificationsController(IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UpdateNotificationsController(IAuthorizationService authorizationService, IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_contentEditingService = contentEditingService;
|
||||
_notificationService = notificationService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public UpdateNotificationsController(IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
contentEditingService,
|
||||
notificationService,
|
||||
backOfficeSecurityAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpPut("{id:guid}/notifications")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateNotifications(CancellationToken cancellationToken, Guid id, UpdateDocumentNotificationsRequestModel updateModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IContent? content = await _contentEditingService.GetAsync(id);
|
||||
if (content == null)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Controllers.UserGroup;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.User;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
@@ -25,11 +28,26 @@ public class UpdateUserGroupsUserController : UserGroupControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public UpdateUserGroupsUserController(IAuthorizationService authorizationService, IUserGroupService userGroupService)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UpdateUserGroupsUserController(
|
||||
IAuthorizationService authorizationService,
|
||||
IUserGroupService userGroupService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_userGroupService = userGroupService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor accepting all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public UpdateUserGroupsUserController(IAuthorizationService authorizationService, IUserGroupService userGroupService)
|
||||
: this(
|
||||
authorizationService,
|
||||
userGroupService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPost("set-user-groups")]
|
||||
@@ -51,7 +69,8 @@ public class UpdateUserGroupsUserController : UserGroupControllerBase
|
||||
|
||||
Attempt<UserGroupOperationStatus> result = await _userGroupService.UpdateUserGroupsOnUsersAsync(
|
||||
requestModel.UserGroupIds.Select(x => x.Id).ToHashSet(),
|
||||
requestModel.UserIds.Select(x => x.Id).ToHashSet());
|
||||
requestModel.UserIds.Select(x => x.Id).ToHashSet(),
|
||||
CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
|
||||
@@ -81,46 +81,23 @@ public class UserEditorAuthorizationHelper
|
||||
return Attempt<string?>.Succeed();
|
||||
}
|
||||
|
||||
// d) a non-admin user can remove any groups but can only add groups they themselves belong to
|
||||
if (userGroupAliases != null)
|
||||
{
|
||||
var savingGroupAliases = userGroupAliases.ToArray();
|
||||
var existingGroupAliases = savingUser == null
|
||||
IEnumerable<string> requestedGroupAliases = userGroupAliases.ToArray();
|
||||
IEnumerable<string> existingGroupAliases = savingUser == null
|
||||
? []
|
||||
: savingUser.Groups.Select(x => x.Alias).ToArray();
|
||||
: savingUser.Groups.Select(x => x.Alias);
|
||||
IEnumerable<string> performingUserGroupAliases = currentUser?.Groups.Select(x => x.Alias) ?? Enumerable.Empty<string>();
|
||||
|
||||
IEnumerable<string> addedGroupAliases = savingGroupAliases.Except(existingGroupAliases);
|
||||
IReadOnlyList<string> unauthorized = UserGroupAssignmentAuthorization
|
||||
.GetUnauthorizedGroupAssignments(performingUserGroupAliases, requestedGroupAliases, existingGroupAliases);
|
||||
|
||||
// As we know the current user is not admin, it is only allowed to use groups that the user do have themselves.
|
||||
var savingGroupAliasesNotAllowed = addedGroupAliases
|
||||
.Except(currentUser?.Groups.Select(x => x.Alias) ?? Enumerable.Empty<string>()).ToArray();
|
||||
if (savingGroupAliasesNotAllowed.Any())
|
||||
if (unauthorized.Count > 0)
|
||||
{
|
||||
return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", savingGroupAliasesNotAllowed) +
|
||||
return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", unauthorized) +
|
||||
"', the current user is not part of them or admin");
|
||||
}
|
||||
|
||||
// only validate any groups that have changed.
|
||||
// a non-admin user can remove groups and add groups that they have access to
|
||||
// but they cannot add a group that they do not have access to or that grants them
|
||||
// path or section access that they don't have access to.
|
||||
var newGroups = savingUser == null
|
||||
? savingGroupAliases
|
||||
: savingGroupAliases.Except(savingUser.Groups.Select(x => x.Alias)).ToArray();
|
||||
|
||||
var userGroupsChanged = savingUser != null && newGroups.Length > 0;
|
||||
|
||||
if (userGroupsChanged)
|
||||
{
|
||||
// d) A user cannot assign a group to another user that they do not belong to
|
||||
var currentUserGroups = currentUser?.Groups.Select(x => x.Alias).ToArray();
|
||||
foreach (var group in newGroups)
|
||||
{
|
||||
if (currentUserGroups?.Contains(group) == false)
|
||||
{
|
||||
return Attempt.Fail("Cannot assign the group " + group + ", the current user is not a member");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Attempt<string?>.Succeed();
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace Umbraco.Cms.Core.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Shared authorization logic for user group assignment.
|
||||
/// </summary>
|
||||
public static class UserGroupAssignmentAuthorization
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the group aliases that the performing user is not authorized to assign.
|
||||
/// </summary>
|
||||
/// <param name="performingUserGroupAliases">The group aliases the performing user belongs to.</param>
|
||||
/// <param name="requestedGroupAliases">The group aliases being assigned to the target user.</param>
|
||||
/// <param name="existingGroupAliases">The group aliases the target user currently belongs to.</param>
|
||||
/// <returns>
|
||||
/// Group aliases that are being added but the performing user does not belong to.
|
||||
/// An empty collection means the assignment is authorized.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Non-admin users can remove any groups but can only add groups they themselves belong to.
|
||||
/// Callers should check for admin status before calling this method, as admins bypass this check.
|
||||
/// </remarks>
|
||||
public static IReadOnlyList<string> GetUnauthorizedGroupAssignments(
|
||||
IEnumerable<string> performingUserGroupAliases,
|
||||
IEnumerable<string> requestedGroupAliases,
|
||||
IEnumerable<string> existingGroupAliases)
|
||||
{
|
||||
var performingGroups = performingUserGroupAliases.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
|
||||
var existingGroups = existingGroupAliases.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
return requestedGroupAliases
|
||||
.Where(alias => existingGroups.Contains(alias) is false && performingGroups.Contains(alias) is false)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
@@ -107,8 +107,25 @@ public interface IUserGroupService
|
||||
/// <param name="userGroupKeys">The user groups the users should be part of.</param>
|
||||
/// <param name="userKeys">The user whose groups we want to alter.</param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
[Obsolete("Please use the overload accepting all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(ISet<Guid> userGroupKeys, ISet<Guid> userKeys);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the users to have the groups specified, with authorization checks based on the performing user.
|
||||
/// </summary>
|
||||
/// <param name="userGroupKeys">The user groups the users should be part of.</param>
|
||||
/// <param name="userKeys">The user whose groups we want to alter.</param>
|
||||
/// <param name="performingUserKey">The key of the user performing the operation.</param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
/// <remarks>
|
||||
/// Non-admin users can only add groups they themselves belong to. Removing groups is always allowed.
|
||||
/// </remarks>
|
||||
// TODO (V18): Remove default implementation.
|
||||
Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(ISet<Guid> userGroupKeys, ISet<Guid> userKeys, Guid performingUserKey)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
=> UpdateUserGroupsOnUsersAsync(userGroupKeys, userKeys);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
Task<Attempt<UserGroupOperationStatus>> AddUsersToUserGroupAsync(UsersToUserGroupManipulationModel addUsersModel, Guid performingUserKey);
|
||||
Task<Attempt<UserGroupOperationStatus>> RemoveUsersFromUserGroupAsync(UsersToUserGroupManipulationModel removeUsersModel, Guid performingUserKey);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.Core.Services.AuthorizationStatus;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
@@ -210,9 +211,24 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
public async Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(
|
||||
// TODO (V19): Collapse the following three methods into a single one, once the obsolete overload
|
||||
// of UpdateUserGroupsOnUsersAsync is removed from the interface.
|
||||
|
||||
public Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(
|
||||
ISet<Guid> userGroupKeys,
|
||||
ISet<Guid> userKeys)
|
||||
=> UpdateUserGroupsOnUsersInternalAsync(userGroupKeys, userKeys, performingUserKey: null);
|
||||
|
||||
public Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(
|
||||
ISet<Guid> userGroupKeys,
|
||||
ISet<Guid> userKeys,
|
||||
Guid performingUserKey)
|
||||
=> UpdateUserGroupsOnUsersInternalAsync(userGroupKeys, userKeys, performingUserKey);
|
||||
|
||||
private async Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersInternalAsync(
|
||||
ISet<Guid> userGroupKeys,
|
||||
ISet<Guid> userKeys,
|
||||
Guid? performingUserKey)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
|
||||
@@ -222,6 +238,40 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
.Select(x => x.ToReadOnlyGroup())
|
||||
.ToArray();
|
||||
|
||||
// Authorize the performing user if provided.
|
||||
if (performingUserKey.HasValue)
|
||||
{
|
||||
IUser? performingUser = await _userService.GetAsync(performingUserKey.Value);
|
||||
if (performingUser is null)
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.Fail(UserGroupOperationStatus.MissingUser);
|
||||
}
|
||||
|
||||
if (performingUser.IsAdmin() is false)
|
||||
{
|
||||
string[] performingUserGroupAliases = performingUser.Groups.Select(g => g.Alias).ToArray();
|
||||
string[] requestedGroupAliases = userGroups.Select(g => g.Alias).ToArray();
|
||||
|
||||
foreach (IUser user in users)
|
||||
{
|
||||
IEnumerable<string> existingGroupAliases = user.Groups.Select(g => g.Alias);
|
||||
|
||||
IReadOnlyList<string> unauthorized = UserGroupAssignmentAuthorization
|
||||
.GetUnauthorizedGroupAssignments(performingUserGroupAliases, requestedGroupAliases, existingGroupAliases);
|
||||
|
||||
if (unauthorized.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"The performing user is not allowed to assign user group(s) '{GroupAliases}' because they do not belong to them.",
|
||||
string.Join(", ", unauthorized));
|
||||
scope.Complete();
|
||||
return Attempt.Fail(UserGroupOperationStatus.Unauthorized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This means that we're potentially de-admining a user, which might cause the admin group to be empty.
|
||||
if (userGroupKeys.Contains(Constants.Security.AdminGroupKey) is false)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"license": "MIT",
|
||||
"version": "16.5.0-rc",
|
||||
"version": "16.6.0-rc",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": null,
|
||||
|
||||
+5
-2
@@ -420,6 +420,8 @@ export abstract class UmbContentDetailWorkspaceContextBase<
|
||||
const repo = new UmbDataTypeDetailRepository(this);
|
||||
|
||||
const propertyTypes = await this.structure.getContentTypeProperties();
|
||||
const contentTypeVariesByCulture = this.structure.getVariesByCulture();
|
||||
const contentTypeVariesBySegment = this.structure.getVariesBySegment();
|
||||
const valueDefinitions = await Promise.all(
|
||||
propertyTypes.map(async (property) => {
|
||||
// TODO: Implement caching for data-type requests. [NL]
|
||||
@@ -438,8 +440,9 @@ export abstract class UmbContentDetailWorkspaceContextBase<
|
||||
propertyEditorSchemaAlias: dataType.editorAlias,
|
||||
config: dataType.values,
|
||||
typeArgs: {
|
||||
variesByCulture: property.variesByCulture,
|
||||
variesBySegment: property.variesBySegment,
|
||||
// Only vary if the content type varies:
|
||||
variesByCulture: contentTypeVariesByCulture ? property.variesByCulture : false,
|
||||
variesBySegment: contentTypeVariesBySegment ? property.variesBySegment : false,
|
||||
} as UmbPropertyTypePresetModelTypeModel,
|
||||
} as UmbPropertyTypePresetModel;
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { UmbMarked } from './ufm.context.js';
|
||||
|
||||
describe('UmbMarked sanitization', () => {
|
||||
describe('XSS prevention on custom elements', () => {
|
||||
it('should strip onclick from custom elements', async () => {
|
||||
const markup = await UmbMarked.parseInline('<uui-button onclick="alert(1)">Click</uui-button>');
|
||||
expect(markup).to.not.include('onclick');
|
||||
expect(markup).to.include('<uui-button>');
|
||||
});
|
||||
|
||||
it('should strip onload from custom elements', async () => {
|
||||
const markup = await UmbMarked.parseInline('<umb-test onload="alert(1)">Test</umb-test>');
|
||||
expect(markup).to.not.include('onload');
|
||||
expect(markup).to.include('<umb-test>');
|
||||
});
|
||||
|
||||
it('should strip onmouseover from custom elements', async () => {
|
||||
const markup = await UmbMarked.parseInline('<uui-box onmouseover="alert(1)">Hover</uui-box>');
|
||||
expect(markup).to.not.include('onmouseover');
|
||||
expect(markup).to.include('<uui-box>');
|
||||
});
|
||||
|
||||
it('should strip onfocus from custom elements', async () => {
|
||||
const markup = await UmbMarked.parseInline('<ufm-label-value onfocus="alert(1)" alias="test"></ufm-label-value>');
|
||||
expect(markup).to.not.include('onfocus');
|
||||
expect(markup).to.include('alias="test"');
|
||||
});
|
||||
|
||||
it('should strip onerror from custom elements', async () => {
|
||||
const markup = await UmbMarked.parseInline('<umb-test onerror="alert(1)">Test</umb-test>');
|
||||
expect(markup).to.not.include('onerror');
|
||||
});
|
||||
});
|
||||
|
||||
describe('safe attributes on custom elements', () => {
|
||||
it('should preserve class attribute', async () => {
|
||||
const markup = await UmbMarked.parseInline('<uui-button class="primary">Click</uui-button>');
|
||||
expect(markup).to.include('class="primary"');
|
||||
});
|
||||
|
||||
it('should preserve alias attribute', async () => {
|
||||
const markup = await UmbMarked.parseInline('<ufm-label-value alias="prop1"></ufm-label-value>');
|
||||
expect(markup).to.include('alias="prop1"');
|
||||
});
|
||||
|
||||
it('should preserve look attribute', async () => {
|
||||
const markup = await UmbMarked.parseInline('<uui-button look="primary">Click</uui-button>');
|
||||
expect(markup).to.include('look="primary"');
|
||||
});
|
||||
|
||||
it('should preserve slot attribute', async () => {
|
||||
const markup = await UmbMarked.parseInline('<umb-test slot="header">Test</umb-test>');
|
||||
expect(markup).to.include('slot="header"');
|
||||
});
|
||||
|
||||
it('should preserve label attribute', async () => {
|
||||
const markup = await UmbMarked.parseInline('<uui-button label="Submit">Click</uui-button>');
|
||||
expect(markup).to.include('label="Submit"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('standard HTML sanitization', () => {
|
||||
it('should strip script tags', async () => {
|
||||
const markup = await UmbMarked.parseInline('<script>alert(1)</script>');
|
||||
expect(markup).to.not.include('<script');
|
||||
});
|
||||
|
||||
it('should allow standard markdown bold', async () => {
|
||||
const markup = await UmbMarked.parseInline('**bold text**');
|
||||
expect(markup).to.include('<strong>bold text</strong>');
|
||||
});
|
||||
|
||||
it('should allow standard markdown links', async () => {
|
||||
const markup = await UmbMarked.parseInline('[link](https://example.com)');
|
||||
expect(markup).to.include('<a');
|
||||
expect(markup).to.include('href="https://example.com"');
|
||||
});
|
||||
|
||||
it('should strip non-allowed custom elements', async () => {
|
||||
const markup = await UmbMarked.parseInline('<custom-evil onclick="alert(1)">Test</custom-evil>');
|
||||
expect(markup).to.not.include('onclick');
|
||||
expect(markup).to.not.include('<custom-evil');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ const UmbDomPurifyConfig: DOMPurifyConfig = {
|
||||
USE_PROFILES: { html: true },
|
||||
CUSTOM_ELEMENT_HANDLING: {
|
||||
tagNameCheck: /^(?:ufm|umb|uui)-.*$/,
|
||||
attributeNameCheck: /.+/,
|
||||
attributeNameCheck: /^(?!on)/,
|
||||
allowCustomizedBuiltInElements: false,
|
||||
},
|
||||
};
|
||||
|
||||
+4
-4
@@ -8,7 +8,7 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.40",
|
||||
"@umbraco/playwright-testhelpers": "^16.0.58",
|
||||
"@umbraco/playwright-testhelpers": "^16.0.60",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
@@ -67,9 +67,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@umbraco/playwright-testhelpers": {
|
||||
"version": "16.0.58",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.58.tgz",
|
||||
"integrity": "sha512-8NWupbb526Ni3eDF/0dVGDSuIYGPP+4lGxtcpTtA0/0p3Ud/9vTYym/k+KgBElDdlY278yXaWez0IpKhShdLOw==",
|
||||
"version": "16.0.60",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.60.tgz",
|
||||
"integrity": "sha512-/6CS4YtsNN3vtahaG35xj5BNYlJMcIPKdiUBrmilWCYAFzxIT9u6EegWBtua46bQxcdeqQgUvIAghsNvDPEk1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "2.0.40",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.40",
|
||||
"@umbraco/playwright-testhelpers": "^16.0.58",
|
||||
"@umbraco/playwright-testhelpers": "^16.0.60",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security;
|
||||
|
||||
[TestFixture]
|
||||
public class UserGroupAssignmentAuthorizationTests
|
||||
{
|
||||
[Test]
|
||||
public void Returns_Empty_When_No_Groups_Are_Being_Added()
|
||||
{
|
||||
var result = UserGroupAssignmentAuthorization.GetUnauthorizedGroupAssignments(
|
||||
performingUserGroupAliases: ["editor"],
|
||||
requestedGroupAliases: ["editor"],
|
||||
existingGroupAliases: ["editor"]);
|
||||
|
||||
Assert.IsEmpty(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_Empty_When_Added_Groups_Belong_To_Performer()
|
||||
{
|
||||
var result = UserGroupAssignmentAuthorization.GetUnauthorizedGroupAssignments(
|
||||
performingUserGroupAliases: ["editor", "writer"],
|
||||
requestedGroupAliases: ["editor", "writer"],
|
||||
existingGroupAliases: ["editor"]);
|
||||
|
||||
Assert.IsEmpty(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_Unauthorized_Groups_Not_Belonging_To_Performer()
|
||||
{
|
||||
var result = UserGroupAssignmentAuthorization.GetUnauthorizedGroupAssignments(
|
||||
performingUserGroupAliases: ["editor"],
|
||||
requestedGroupAliases: ["editor", "admin"],
|
||||
existingGroupAliases: ["editor"]);
|
||||
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual("admin", result[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Existing_Groups_Are_Not_Flagged_Even_If_Performer_Does_Not_Belong()
|
||||
{
|
||||
var result = UserGroupAssignmentAuthorization.GetUnauthorizedGroupAssignments(
|
||||
performingUserGroupAliases: ["editor"],
|
||||
requestedGroupAliases: ["writer", "editor"],
|
||||
existingGroupAliases: ["writer"]);
|
||||
|
||||
Assert.IsEmpty(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_All_Unauthorized_When_Multiple_New_Groups_Fail()
|
||||
{
|
||||
var result = UserGroupAssignmentAuthorization.GetUnauthorizedGroupAssignments(
|
||||
performingUserGroupAliases: ["editor"],
|
||||
requestedGroupAliases: ["editor", "admin", "sensitiveData"],
|
||||
existingGroupAliases: []);
|
||||
|
||||
Assert.AreEqual(2, result.Count);
|
||||
CollectionAssert.AreEquivalent(new[] { "admin", "sensitiveData" }, result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_Empty_When_All_Requested_Groups_Are_Removals()
|
||||
{
|
||||
// Requesting fewer groups than existing = only removals, no additions
|
||||
var result = UserGroupAssignmentAuthorization.GetUnauthorizedGroupAssignments(
|
||||
performingUserGroupAliases: ["editor"],
|
||||
requestedGroupAliases: ["editor"],
|
||||
existingGroupAliases: ["editor", "writer", "admin"]);
|
||||
|
||||
Assert.IsEmpty(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Comparison_Is_Case_Insensitive()
|
||||
{
|
||||
var result = UserGroupAssignmentAuthorization.GetUnauthorizedGroupAssignments(
|
||||
performingUserGroupAliases: ["Editor"],
|
||||
requestedGroupAliases: ["editor", "Writer"],
|
||||
existingGroupAliases: ["WRITER"]);
|
||||
|
||||
Assert.IsEmpty(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_Empty_When_No_Groups_Requested()
|
||||
{
|
||||
var result = UserGroupAssignmentAuthorization.GetUnauthorizedGroupAssignments(
|
||||
performingUserGroupAliases: ["editor"],
|
||||
requestedGroupAliases: [],
|
||||
existingGroupAliases: ["editor", "writer"]);
|
||||
|
||||
Assert.IsEmpty(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_Empty_When_Performer_Has_No_Groups_But_Nothing_Added()
|
||||
{
|
||||
var result = UserGroupAssignmentAuthorization.GetUnauthorizedGroupAssignments(
|
||||
performingUserGroupAliases: [],
|
||||
requestedGroupAliases: ["editor"],
|
||||
existingGroupAliases: ["editor"]);
|
||||
|
||||
Assert.IsEmpty(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_Unauthorized_When_Performer_Has_No_Groups_And_Groups_Added()
|
||||
{
|
||||
var result = UserGroupAssignmentAuthorization.GetUnauthorizedGroupAssignments(
|
||||
performingUserGroupAliases: [],
|
||||
requestedGroupAliases: ["editor"],
|
||||
existingGroupAliases: []);
|
||||
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual("editor", result[0]);
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,276 @@ public class UserGroupServiceTests
|
||||
Assert.AreEqual(status, updateAttempt.Status);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateUserGroupsOnUsers_Admin_Can_Assign_Any_Groups()
|
||||
{
|
||||
// Arrange - admin performing user assigning a group they don't belong to (sensitive)
|
||||
// This verifies the admin bypass: without it, assigning "sensitive" would be unauthorized.
|
||||
var performingUserKey = Guid.NewGuid();
|
||||
var targetUserKey = Guid.NewGuid();
|
||||
var sensitiveGroupKey = Constants.Security.SensitiveDataGroupKey;
|
||||
var adminGroupKey = Constants.Security.AdminGroupKey;
|
||||
|
||||
var service = SetupUserGroupServiceForUpdateUserGroups(
|
||||
performingUserKey,
|
||||
performingUserGroupAliases: [Constants.Security.AdminGroupAlias],
|
||||
targetUserKey,
|
||||
targetUserCurrentGroupAliases: ["editor"],
|
||||
requestedGroups: [(adminGroupKey, Constants.Security.AdminGroupAlias), (sensitiveGroupKey, "sensitive")]);
|
||||
|
||||
// Act
|
||||
var result = await service.UpdateUserGroupsOnUsersAsync(
|
||||
new HashSet<Guid> { adminGroupKey, sensitiveGroupKey },
|
||||
new HashSet<Guid> { targetUserKey },
|
||||
performingUserKey);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.Success, result.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateUserGroupsOnUsers_NonAdmin_Can_Add_Groups_They_Belong_To()
|
||||
{
|
||||
// Arrange - Editor performing user adding Editor group to a Writer user
|
||||
var performingUserKey = Guid.NewGuid();
|
||||
var targetUserKey = Guid.NewGuid();
|
||||
var writerGroupKey = Guid.NewGuid();
|
||||
var editorGroupKey = Guid.NewGuid();
|
||||
|
||||
var service = SetupUserGroupServiceForUpdateUserGroups(
|
||||
performingUserKey,
|
||||
performingUserGroupAliases: ["editor"],
|
||||
targetUserKey,
|
||||
targetUserCurrentGroupAliases: ["writer"],
|
||||
requestedGroups: [(writerGroupKey, "writer"), (editorGroupKey, "editor")]);
|
||||
|
||||
// Act
|
||||
var result = await service.UpdateUserGroupsOnUsersAsync(
|
||||
new HashSet<Guid> { writerGroupKey, editorGroupKey },
|
||||
new HashSet<Guid> { targetUserKey },
|
||||
performingUserKey);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.Success, result.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateUserGroupsOnUsers_NonAdmin_Cannot_Add_Groups_They_Do_Not_Belong_To()
|
||||
{
|
||||
// Arrange - Editor performing user trying to add Admin group to an Editor user
|
||||
var performingUserKey = Guid.NewGuid();
|
||||
var targetUserKey = Guid.NewGuid();
|
||||
var editorGroupKey = Guid.NewGuid();
|
||||
var adminGroupKey = Constants.Security.AdminGroupKey;
|
||||
|
||||
var service = SetupUserGroupServiceForUpdateUserGroups(
|
||||
performingUserKey,
|
||||
performingUserGroupAliases: ["editor"],
|
||||
targetUserKey,
|
||||
targetUserCurrentGroupAliases: ["editor"],
|
||||
requestedGroups: [(editorGroupKey, "editor"), (adminGroupKey, Constants.Security.AdminGroupAlias)]);
|
||||
|
||||
// Act
|
||||
var result = await service.UpdateUserGroupsOnUsersAsync(
|
||||
new HashSet<Guid> { editorGroupKey, adminGroupKey },
|
||||
new HashSet<Guid> { targetUserKey },
|
||||
performingUserKey);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.Unauthorized, result.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateUserGroupsOnUsers_NonAdmin_Can_Keep_Existing_Groups_They_Do_Not_Belong_To()
|
||||
{
|
||||
// Arrange - Editor user keeping Writer+Translator (existing) and adding Editor (own group)
|
||||
var performingUserKey = Guid.NewGuid();
|
||||
var targetUserKey = Guid.NewGuid();
|
||||
var writerGroupKey = Guid.NewGuid();
|
||||
var translatorGroupKey = Guid.NewGuid();
|
||||
var editorGroupKey = Guid.NewGuid();
|
||||
|
||||
var service = SetupUserGroupServiceForUpdateUserGroups(
|
||||
performingUserKey,
|
||||
performingUserGroupAliases: ["editor"],
|
||||
targetUserKey,
|
||||
targetUserCurrentGroupAliases: ["writer", "translator"],
|
||||
requestedGroups: [(writerGroupKey, "writer"), (translatorGroupKey, "translator"), (editorGroupKey, "editor")]);
|
||||
|
||||
// Act
|
||||
var result = await service.UpdateUserGroupsOnUsersAsync(
|
||||
new HashSet<Guid> { writerGroupKey, translatorGroupKey, editorGroupKey },
|
||||
new HashSet<Guid> { targetUserKey },
|
||||
performingUserKey);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.Success, result.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateUserGroupsOnUsers_NonAdmin_Can_Remove_Groups()
|
||||
{
|
||||
// Arrange - Editor user removing Writer group from a user that has both Writer and Editor
|
||||
var performingUserKey = Guid.NewGuid();
|
||||
var targetUserKey = Guid.NewGuid();
|
||||
var editorGroupKey = Guid.NewGuid();
|
||||
|
||||
var service = SetupUserGroupServiceForUpdateUserGroups(
|
||||
performingUserKey,
|
||||
performingUserGroupAliases: ["editor"],
|
||||
targetUserKey,
|
||||
targetUserCurrentGroupAliases: ["writer", "editor"],
|
||||
requestedGroups: [(editorGroupKey, "editor")]);
|
||||
|
||||
// Act
|
||||
var result = await service.UpdateUserGroupsOnUsersAsync(
|
||||
new HashSet<Guid> { editorGroupKey },
|
||||
new HashSet<Guid> { targetUserKey },
|
||||
performingUserKey);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.Success, result.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateUserGroupsOnUsers_NonAdmin_Cannot_Escalate_Via_Replacement()
|
||||
{
|
||||
// Arrange - Editor user replacing their Editor group with Admin group
|
||||
var performingUserKey = Guid.NewGuid();
|
||||
var targetUserKey = Guid.NewGuid();
|
||||
var adminGroupKey = Constants.Security.AdminGroupKey;
|
||||
|
||||
var service = SetupUserGroupServiceForUpdateUserGroups(
|
||||
performingUserKey,
|
||||
performingUserGroupAliases: ["editor"],
|
||||
targetUserKey,
|
||||
targetUserCurrentGroupAliases: ["editor"],
|
||||
requestedGroups: [(adminGroupKey, Constants.Security.AdminGroupAlias)]);
|
||||
|
||||
// Act
|
||||
var result = await service.UpdateUserGroupsOnUsersAsync(
|
||||
new HashSet<Guid> { adminGroupKey },
|
||||
new HashSet<Guid> { targetUserKey },
|
||||
performingUserKey);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.Unauthorized, result.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateUserGroupsOnUsers_Missing_Performing_User_Returns_MissingUser()
|
||||
{
|
||||
// Arrange - performing user key that doesn't resolve to a user
|
||||
var performingUserKey = Guid.NewGuid();
|
||||
var targetUserKey = Guid.NewGuid();
|
||||
var editorGroupKey = Guid.NewGuid();
|
||||
|
||||
var scope = new Mock<ICoreScope>();
|
||||
var provider = new Mock<ICoreScopeProvider>();
|
||||
provider.Setup(p => p.CreateCoreScope(
|
||||
It.IsAny<IsolationLevel>(),
|
||||
It.IsAny<RepositoryCacheMode>(),
|
||||
It.IsAny<IEventDispatcher?>(),
|
||||
It.IsAny<IScopedNotificationPublisher?>(),
|
||||
It.IsAny<bool?>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<bool>()))
|
||||
.Returns(scope.Object);
|
||||
|
||||
var query = new Mock<IQuery<IUserGroup>>();
|
||||
query.Setup(q => q.Where(It.IsAny<Expression<Func<IUserGroup, bool>>>())).Returns(query.Object);
|
||||
provider.Setup(p => p.CreateQuery<IUserGroup>()).Returns(query.Object);
|
||||
|
||||
var targetUser = SetupUserWithGroupAccess(targetUserKey, ["editor"]);
|
||||
var userService = new Mock<IUserService>();
|
||||
// Performing user not found
|
||||
userService.Setup(s => s.GetAsync(performingUserKey)).Returns(Task.FromResult<IUser?>(null));
|
||||
userService.Setup(s => s.GetAsync(It.IsAny<IEnumerable<Guid>>()))
|
||||
.Returns(Task.FromResult<IEnumerable<IUser>>(new[] { targetUser.Object }));
|
||||
|
||||
var userGroupRepository = new Mock<IUserGroupRepository>();
|
||||
userGroupRepository
|
||||
.Setup(r => r.Get(It.IsAny<IQuery<IUserGroup>>()))
|
||||
.Returns(new[] { new UserGroup(Mock.Of<IShortStringHelper>(), 0, "editor", "Editor", null) { Key = editorGroupKey } });
|
||||
|
||||
var service = new UserGroupService(
|
||||
provider.Object,
|
||||
Mock.Of<ILoggerFactory>(),
|
||||
Mock.Of<IEventMessagesFactory>(),
|
||||
userGroupRepository.Object,
|
||||
Mock.Of<IUserGroupPermissionService>(),
|
||||
Mock.Of<IEntityService>(),
|
||||
userService.Object,
|
||||
Mock.Of<ILogger<UserGroupService>>());
|
||||
|
||||
// Act
|
||||
var result = await service.UpdateUserGroupsOnUsersAsync(
|
||||
new HashSet<Guid> { editorGroupKey },
|
||||
new HashSet<Guid> { targetUserKey },
|
||||
performingUserKey);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.MissingUser, result.Result);
|
||||
}
|
||||
|
||||
private UserGroupService SetupUserGroupServiceForUpdateUserGroups(
|
||||
Guid performingUserKey,
|
||||
string[] performingUserGroupAliases,
|
||||
Guid targetUserKey,
|
||||
string[] targetUserCurrentGroupAliases,
|
||||
(Guid Key, string Alias)[] requestedGroups)
|
||||
{
|
||||
var scope = new Mock<ICoreScope>();
|
||||
var provider = new Mock<ICoreScopeProvider>();
|
||||
provider.Setup(p => p.CreateCoreScope(
|
||||
It.IsAny<IsolationLevel>(),
|
||||
It.IsAny<RepositoryCacheMode>(),
|
||||
It.IsAny<IEventDispatcher?>(),
|
||||
It.IsAny<IScopedNotificationPublisher?>(),
|
||||
It.IsAny<bool?>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<bool>()))
|
||||
.Returns(scope.Object);
|
||||
|
||||
var query = new Mock<IQuery<IUserGroup>>();
|
||||
query.Setup(q => q.Where(It.IsAny<Expression<Func<IUserGroup, bool>>>())).Returns(query.Object);
|
||||
provider.Setup(p => p.CreateQuery<IUserGroup>()).Returns(query.Object);
|
||||
|
||||
var performingUser = SetupUserWithGroupAccess(performingUserKey, performingUserGroupAliases);
|
||||
var targetUser = SetupUserWithGroupAccess(targetUserKey, targetUserCurrentGroupAliases);
|
||||
|
||||
var userService = new Mock<IUserService>();
|
||||
userService.Setup(s => s.GetAsync(performingUserKey)).Returns(Task.FromResult<IUser?>(performingUser.Object));
|
||||
userService.Setup(s => s.GetAsync(It.IsAny<IEnumerable<Guid>>()))
|
||||
.Returns(Task.FromResult<IEnumerable<IUser>>(new[] { targetUser.Object }));
|
||||
|
||||
IUserGroup[] userGroups = requestedGroups
|
||||
.Select(g => new UserGroup(Mock.Of<IShortStringHelper>(), 0, g.Alias, g.Alias, null) { Key = g.Key })
|
||||
.ToArray<IUserGroup>();
|
||||
|
||||
var userGroupRepository = new Mock<IUserGroupRepository>();
|
||||
userGroupRepository
|
||||
.Setup(r => r.Get(It.IsAny<IQuery<IUserGroup>>()))
|
||||
.Returns(userGroups);
|
||||
|
||||
return new UserGroupService(
|
||||
provider.Object,
|
||||
Mock.Of<ILoggerFactory>(),
|
||||
Mock.Of<IEventMessagesFactory>(),
|
||||
userGroupRepository.Object,
|
||||
Mock.Of<IUserGroupPermissionService>(),
|
||||
Mock.Of<IEntityService>(),
|
||||
userService.Object,
|
||||
Mock.Of<ILogger<UserGroupService>>());
|
||||
}
|
||||
|
||||
private IEnumerable<IReadOnlyUserGroup> CreateGroups(params string[] aliases)
|
||||
=> aliases.Select(alias =>
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
|
||||
"version": "16.5.0-rc",
|
||||
"version": "16.6.0-rc",
|
||||
"assemblyVersion": {
|
||||
"precision": "build"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user