Add user group filter endpoint (#16087)

This commit is contained in:
Kenn Jacobsen
2024-04-30 14:55:20 +02:00
committed by GitHub
parent 39e51a4467
commit de230334be
7 changed files with 333 additions and 5 deletions

View File

@@ -0,0 +1,64 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup;
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;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.User.Filter;
[ApiVersion("1.0")]
public class FilterUserGroupFilterController : UserGroupFilterControllerBase
{
private readonly IUserGroupService _userGroupService;
private readonly IUserGroupPresentationFactory _userGroupPresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public FilterUserGroupFilterController(
IUserGroupService userGroupService,
IUserGroupPresentationFactory userGroupPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_userGroupService = userGroupService;
_userGroupPresentationFactory = userGroupPresentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<UserGroupResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Filter(
CancellationToken cancellationToken,
int skip = 0,
int take = 100,
string filter = "")
{
Attempt<PagedModel<IUserGroup>, UserGroupOperationStatus> filterAttempt = await _userGroupService.FilterAsync(
CurrentUserKey(_backOfficeSecurityAccessor),
filter,
skip,
take);
if (filterAttempt.Success is false)
{
return UserGroupOperationStatusResult(filterAttempt.Status);
}
IEnumerable<UserGroupResponseModel> viewModels = await _userGroupPresentationFactory.CreateMultipleAsync(filterAttempt.Result.Items);
var responseModel = new PagedViewModel<UserGroupResponseModel>
{
Total = filterAttempt.Result.Total,
Items = viewModels,
};
return Ok(responseModel);
}
}

View File

@@ -0,0 +1,10 @@
using Umbraco.Cms.Api.Management.Controllers.UserGroup;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.User.Filter;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Filter}/user-group")]
public abstract class UserGroupFilterControllerBase : UserGroupControllerBase
{
}

View File

@@ -32,11 +32,6 @@ public class GetAllUserGroupController : UserGroupControllerBase
int skip = 0,
int take = 100)
{
// FIXME: In the old controller this endpoint had a switch "onlyCurrentUserGroup"
// If this was enabled we'd only return the groups the current user was in
// and even if it was set to false we'd still remove the admin group.
// We still need to have this functionality, however, it does not belong here.
// Instead we should implement this functionality on the CurrentUserController
PagedModel<IUserGroup> userGroups = await _userGroupService.GetAllAsync(skip, take);
var viewModels = (await _userPresentationFactory.CreateMultipleAsync(userGroups.Items)).ToList();

View File

@@ -27634,6 +27634,97 @@
]
}
},
"/umbraco/management/api/v1/filter/user-group": {
"get": {
"tags": [
"User Group"
],
"operationId": "GetFilterUserGroup",
"parameters": [
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 100
}
},
{
"name": "filter",
"in": "query",
"schema": {
"type": "string",
"default": ""
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PagedUserGroupResponseModel"
}
]
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user do not have access to this resource"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/item/user-group": {
"get": {
"tags": [

View File

@@ -62,6 +62,19 @@ public interface IUserGroupService
Task<IEnumerable<IUserGroup>> GetAsync(IEnumerable<Guid> keys);
/// <summary>
/// Performs filtering for user groups
/// </summary>
/// <param name="userKey">The key of the performing (current) user.</param>
/// <param name="filter">The filter to apply.</param>
/// <param name="skip">The amount of user groups to skip.</param>
/// <param name="take">The amount of user groups to take.</param>
/// <returns>All matching user groups as an enumerable list of <see cref="IUserGroup"/>.</returns>
/// <remarks>
/// If the performing user is not an administrator, this method only returns groups that the performing user is a member of.
/// </remarks>
Task<Attempt<PagedModel<IUserGroup>, UserGroupOperationStatus>> FilterAsync(Guid userKey, string? filter, int skip, int take);
/// <summary>
/// Persists a new user group.
/// </summary>

View File

@@ -134,6 +134,35 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
return Task.FromResult<IEnumerable<IUserGroup>>(result);
}
/// <inheritdoc/>
public async Task<Attempt<PagedModel<IUserGroup>, UserGroupOperationStatus>> FilterAsync(Guid userKey, string? filter, int skip, int take)
{
IUser? requestingUser = await _userService.GetAsync(userKey);
if (requestingUser is null)
{
return Attempt.FailWithStatus(UserGroupOperationStatus.MissingUser, new PagedModel<IUserGroup>());
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
var groups = _userGroupRepository
.GetMany()
.Where(group => filter.IsNullOrWhiteSpace() || group.Name?.InvariantContains(filter) is true)
.OrderBy(group => group.Name)
.ToList();
if (requestingUser.IsAdmin() is false)
{
var requestingUserGroups = requestingUser.Groups.Select(group => group.Alias).ToArray();
groups.RemoveAll(group =>
group.Alias is Constants.Security.AdminGroupAlias
|| requestingUserGroups.Contains(group.Alias) is false);
}
return Attempt.SucceedWithStatus(
UserGroupOperationStatus.Success,
new PagedModel<IUserGroup> { Items = groups.Skip(skip).Take(take), Total = groups.Count });
}
/// <inheritdoc/>
public async Task<Attempt<UserGroupOperationStatus>> DeleteAsync(ISet<Guid> keys)
{

View File

@@ -0,0 +1,126 @@
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
[TestFixture]
public class UserGroupServiceTests
{
[TestCase("one", "two", "three")]
[TestCase("two", "three")]
[TestCase("three")]
[TestCase]
public async Task Filter_Returns_Only_User_Groups_For_Non_Admin(params string[] userGroupAliases)
{
var userKey = Guid.NewGuid();
var userGroupService = SetupUserGroupService(userKey, userGroupAliases);
var result = await userGroupService.FilterAsync(userKey, null, 0, 10);
Assert.Multiple(() =>
{
Assert.IsTrue(result.Success);
Assert.AreEqual(userGroupAliases.Length, result.Result.Items.Count());
foreach (var userGroupAlias in userGroupAliases)
{
Assert.IsNotNull(result.Result.Items.SingleOrDefault(g => g.Alias == userGroupAlias));
}
});
}
[TestCase("four", "five", "six")]
[TestCase("four")]
[TestCase]
public async Task Filter_Does_Not_Return_Non_Existing_Groups(params string[] userGroupAliases)
{
var userKey = Guid.NewGuid();
var userGroupService = SetupUserGroupService(userKey, userGroupAliases);
var result = await userGroupService.FilterAsync(userKey, null, 0, 10);
Assert.Multiple(() =>
{
Assert.IsTrue(result.Success);
Assert.IsEmpty(result.Result.Items);
});
}
[Test]
public async Task Filter_Returns_All_Groups_For_Admin()
{
var userKey = Guid.NewGuid();
var userGroupService = SetupUserGroupService(userKey, new [] { Constants.Security.AdminGroupAlias });
var result = await userGroupService.FilterAsync(userKey, null, 0, 10);
Assert.Multiple(() =>
{
Assert.IsTrue(result.Success);
Assert.AreEqual(4, result.Result.Items.Count());
Assert.IsNotNull(result.Result.Items.SingleOrDefault(g => g.Alias == Constants.Security.AdminGroupAlias));
Assert.IsNotNull(result.Result.Items.SingleOrDefault(g => g.Alias == "one"));
Assert.IsNotNull(result.Result.Items.SingleOrDefault(g => g.Alias == "two"));
Assert.IsNotNull(result.Result.Items.SingleOrDefault(g => g.Alias == "three"));
});
}
[Test]
public async Task Filter_Can_Filter_By_Group_Name()
{
var userKey = Guid.NewGuid();
var userGroupService = SetupUserGroupService(userKey, new [] { Constants.Security.AdminGroupAlias });
var result = await userGroupService.FilterAsync(userKey, "e", 0, 10);
Assert.Multiple(() =>
{
Assert.IsTrue(result.Success);
Assert.AreEqual(2, result.Result.Items.Count());
Assert.IsNotNull(result.Result.Items.SingleOrDefault(g => g.Alias == "one"));
Assert.IsNotNull(result.Result.Items.SingleOrDefault(g => g.Alias == "three"));
});
}
private IEnumerable<IReadOnlyUserGroup> CreateGroups(params string[] aliases)
=> aliases.Select(alias =>
{
var group = new Mock<IReadOnlyUserGroup>();
group.SetupGet(g => g.Alias).Returns(alias);
return group.Object;
}).ToArray();
private IUserGroupService SetupUserGroupService(Guid userKey, string[] userGroupAliases)
{
var user = new Mock<IUser>();
user.SetupGet(u => u.Key).Returns(userKey);
user.Setup(u => u.Groups).Returns(CreateGroups(userGroupAliases));
var userService = new Mock<IUserService>();
userService.Setup(s => s.GetAsync(userKey)).Returns(Task.FromResult(user.Object));
var userGroupRepository = new Mock<IUserGroupRepository>();
userGroupRepository
.Setup(r => r.GetMany())
.Returns(new[]
{
new UserGroup(Mock.Of<IShortStringHelper>(), 0, Constants.Security.AdminGroupAlias, "Administrators", null),
new UserGroup(Mock.Of<IShortStringHelper>(), 0, "one", "Group One", null),
new UserGroup(Mock.Of<IShortStringHelper>(), 0, "two", "Group Two", null),
new UserGroup(Mock.Of<IShortStringHelper>(), 0, "three", "Group Three", null),
});
return new UserGroupService(
Mock.Of<ICoreScopeProvider>(),
Mock.Of<ILoggerFactory>(),
Mock.Of<IEventMessagesFactory>(),
userGroupRepository.Object,
Mock.Of<IUserGroupPermissionService>(),
Mock.Of<IEntityService>(),
userService.Object,
Mock.Of<ILogger<UserGroupService>>());
}
}