Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ public async Task<ActionResult<PagedViewModel<UserGroupResponseModel>>> GetAll(
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();
Expand Down
91 changes: 91 additions & 0 deletions src/Umbraco.Cms.Api.Management/OpenApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -27462,6 +27462,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": [
Expand Down
13 changes: 13 additions & 0 deletions src/Umbraco.Core/Services/IUserGroupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
29 changes: 29 additions & 0 deletions src/Umbraco.Core/Services/UserGroupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,35 @@ public Task<IEnumerable<IUserGroup>> GetAsync(IEnumerable<Guid> keys)
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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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>>());
}
}