Add user group filter endpoint (#16087)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user