New Backoffice: User Groups Controller (#13811)
* Add key to UserGroupDto * Fix renaming table in sqlite The SqliteSyntaxProvider needed an overload to use the correct query * Start work on user group GUID migration * Add key index to UserGroupDto * Copy over data when migrating sqlite * Make sqlite column migration work * Remove PostMigrations These should be replaced with Notification usage * Remove outer scope from Upgrader * Remove unececary null check * Add marker base class for migrations * Enable scopeless migrations * Remove unnecessary state check The final state of the migration is no longer necessarily the final state of the plan. * Extend ExecutedMigrationPlan * Ensure that MigrationPlanExecutor.Execute always returns a result. * Always save final state, regardless of errors * Remove obsolete Execute * Add Umbraco specific migration notification * Publish notification after umbraco migration * Throw the exception that failed a migration after publishing notification * Handle notification publishing in DatabaseBuilder * Fix tests * Remember to complete scope * Clean up MigrationPlanExecutor * Run each package migration in a separate scope * Add PartialMigrationsTests * Add unhappy path test * Fix bug shown by test * Move PartialMigrationsTests into the correct folder * Comment out refresh cache in data type migration Need to add this back again as a notification handler or something. * Start working on a notification test * Allow migrations to request a cache rebuild * Set RebuildCache from MigrateDataTypeConfigurations * Clean MigrationPlanExecutor * Add comment explaining the need to partial migration success * Fix tests * Allow overriding DefinePlan of UmbracoPlan This is needed to test the DatabaseBuilder * Fix notification test * Don't throw exception to be immediately re-caught * Assert that scopes notification are always published * Ensure that scopes are created when requested * Make test classes internal. It doesn't really matter, but this way it doesn't show up in intellisense * Add notification handler for clearing cookies * Add CompatibilitySuppressions * Use unscoped migration for adding GUID to user group * Make sqlite migration work It's really not pretty, square peg, round hole. * Don't re-enable foreign keys This will happen automatically next time a connection is started. * Scope database when using SQLServer * Don't call complete transaction * Tidy up a couple of comment * Only allow scoping the database from UnscopedMigrationBase * Fix comment * Remove remark in UnscopedMigrationBase as it's no longer true * Add keys when creating default user groups * Map database value from DTO to entity * Fix migration Rename also renamed the foreign keys, making it not work * Make migration idempotent * Fix unit test * Update CompatibilitySuppressions.xml * Add GetUserGroupByKey to UserService * Add ByKey endpoint * Add UniqueId to AppendGroupBy Otherwise MSSQL grenades * Ensure that languages are returned by PerformGetByQuery * add POC displaying model * Clean up by key controller * Add GetAllEndpoint * Add delete endpoint * Use GetKey to get GUID from id Instead of pulling up the entire entity. * Add UserGroup2Permission table * Fetch the new permissions when getting user groups * Dont ToString int to parse it to a short I'm pretty sure this is some way old migration type code that doesn't make any sense anymore * Add new relation to GetDeleteClauses * Persist the permissions * Split UserGroupViewModel into multiple models This is to make it possible to make endpoints more rest-ish * Bootstrap create and update endpoints * Make GetAllUserGroupController paged * Add method to create IUserGroup from UserGroupSaveModel * Add sanity check version of endpoint * Fix persisting permissions * Map section aliases to the name the frontend expects This is a temporary fix till we find out how we really want to handle this * Fix up post merge * Make naming more consistent * Implement initial update endpoint * Fix media start node * Clean name for XSS when mapping to IUserGroup * Use a set instead of a list for permission names We don't want dupes * Make permission column nvarchar max * Add UserGroupOperationStatuses * Add IUserGroupAuthorizationService * Add specific user group creation method to user service * Move validating and authorizing into its own methods * Add operation result to action result mapping * Update create controller to use the create method * Fix create end point * Comment out getting current user untill we have auth * Add usergroup service * Obsolete usergroup things from IUserService * Add update to UserGroupService interface * User IUserGroupService in controllers * User async notifications overloads * Move authorize user group creation into its own service * Add AuthorizeUserGroupUpdate method * Make new service implementations internal and sealed * Add update user * Add GetAll to usergroup service * Remove or obsolete usages of GetAllUserGroups * Add usergroup service to DI * Remove usage of GetGroupsByAlias * Remove usages of GetUserGroupByAlias * Remove usage of GetUserGroupById * Add new table when creating a new database * Implement Delete * Add skip and take to getall * Move skip take into the service * Fixup suggestions in user group service * Fixup unit tests * Allow admins to change user groups they're not a part of * Add CompatibilitySuppressions * Update openapi * Uppdate OpenApi.json again * Add missing compatibility suppression * Added missing type info in ProducesResponseTypeAttribute * Added INamedEntityViewModel and added on the relevant view models * Fixed bug, resulting in serialization not being the same as swagger reported. Now all types objects implementing an interface, is serialized with the $type property * updated OpenApi.json * Added missing title in notfound response * Typo * .Result to .GetAwaiter().GetResult() * Update comment to mention it should be implemented on CurrentUserController * Validate that start nodes actually exists * Handle not found consistently * Use iso codes instead of ids * Update OpenAPI * Automatically infer statuscode in problemdetails * Ensure that the language exists * Fix usergroup 2 permission index * Validate that group name and alias is not too long * Only return status from validation We're just returning the same usergroups, and this is less boilerplate code * Handle empty and null group names * Remove group prefix from statuses * Add some basic validation tests * Don't allow updating a usergroup to having a duplicate alias --------- Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
@@ -7,7 +7,6 @@ public class ProblemDetailsBuilder
|
||||
{
|
||||
private string? _title;
|
||||
private string? _detail;
|
||||
private int _status = StatusCodes.Status400BadRequest;
|
||||
private string? _type;
|
||||
|
||||
public ProblemDetailsBuilder WithTitle(string title)
|
||||
@@ -22,12 +21,6 @@ public class ProblemDetailsBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProblemDetailsBuilder WithStatus(int status)
|
||||
{
|
||||
_status = status;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProblemDetailsBuilder WithType(string type)
|
||||
{
|
||||
_type = type;
|
||||
@@ -39,7 +32,6 @@ public class ProblemDetailsBuilder
|
||||
{
|
||||
Title = _title,
|
||||
Detail = _detail,
|
||||
Status = _status,
|
||||
Type = _type ?? "Error",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroups;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.UserGroups;
|
||||
|
||||
|
||||
public class ByKeyUserGroupController : UserGroupsControllerBase
|
||||
{
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IUserGroupViewModelFactory _userGroupViewModelFactory;
|
||||
|
||||
public ByKeyUserGroupController(
|
||||
IUserGroupService userGroupService,
|
||||
IUserGroupViewModelFactory userGroupViewModelFactory)
|
||||
{
|
||||
_userGroupService = userGroupService;
|
||||
_userGroupViewModelFactory = userGroupViewModelFactory;
|
||||
}
|
||||
|
||||
[HttpGet("{key:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(UserGroupViewModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<UserGroupViewModel>> ByKey(Guid key)
|
||||
{
|
||||
IUserGroup? userGroup = await _userGroupService.GetAsync(key);
|
||||
|
||||
if (userGroup is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await _userGroupViewModelFactory.CreateAsync(userGroup);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroups;
|
||||
using Umbraco.Cms.Core;
|
||||
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.UserGroups;
|
||||
|
||||
public class CreateUserGroupController : UserGroupsControllerBase
|
||||
{
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IUserGroupViewModelFactory _userGroupViewModelFactory;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public CreateUserGroupController(
|
||||
IUserGroupService userGroupService,
|
||||
IUserGroupViewModelFactory userGroupViewModelFactory,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_userGroupService = userGroupService;
|
||||
_userGroupViewModelFactory = userGroupViewModelFactory;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
public async Task<IActionResult> Create(UserGroupSaveModel userGroupSaveModel)
|
||||
{
|
||||
// FIXME: Comment this in when auth is in place and we can get a currently logged in user.
|
||||
// IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
|
||||
// if (currentUser is null)
|
||||
// {
|
||||
// return UserGroupOperationStatusResult(UserGroupOperationStatus.MissingUser);
|
||||
// }
|
||||
|
||||
Attempt<IUserGroup, UserGroupOperationStatus> userGroupCreationAttempt = await _userGroupViewModelFactory.CreateAsync(userGroupSaveModel);
|
||||
if (userGroupCreationAttempt.Success is false)
|
||||
{
|
||||
return UserGroupOperationStatusResult(userGroupCreationAttempt.Status);
|
||||
}
|
||||
|
||||
IUserGroup group = userGroupCreationAttempt.Result;
|
||||
|
||||
Attempt<IUserGroup, UserGroupOperationStatus> result = await _userGroupService.CreateAsync(group, /*currentUser.Id*/ -1);
|
||||
return result.Success
|
||||
? CreatedAtAction<ByKeyUserGroupController>(controller => nameof(controller.ByKey), group.Key)
|
||||
: UserGroupOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.UserGroups;
|
||||
|
||||
public class DeleteUserGroupController : UserGroupsControllerBase
|
||||
{
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
|
||||
public DeleteUserGroupController(IUserGroupService userGroupService)
|
||||
{
|
||||
_userGroupService = userGroupService;
|
||||
}
|
||||
|
||||
[HttpDelete("{key:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid key)
|
||||
{
|
||||
Attempt<UserGroupOperationStatus> result = await _userGroupService.DeleteAsync(key);
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: UserGroupOperationStatusResult(result.Result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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.UserGroups;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.UserGroups;
|
||||
|
||||
public class GetAllUserGroupController : UserGroupsControllerBase
|
||||
{
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IUserGroupViewModelFactory _userViewModelFactory;
|
||||
|
||||
public GetAllUserGroupController(
|
||||
IUserGroupService userGroupService,
|
||||
IUserGroupViewModelFactory userViewModelFactory)
|
||||
{
|
||||
_userGroupService = userGroupService;
|
||||
_userViewModelFactory = userViewModelFactory;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<UserGroupViewModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<UserGroupViewModel>>> 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 _userViewModelFactory.CreateMultipleAsync(userGroups.Items)).ToList();
|
||||
return new PagedViewModel<UserGroupViewModel> { Total = userGroups.Total, Items = viewModels };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroups;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.UserGroups;
|
||||
|
||||
public class UpdateUserGroupController : UserGroupsControllerBase
|
||||
{
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IUserGroupViewModelFactory _userGroupViewModelFactory;
|
||||
|
||||
public UpdateUserGroupController(
|
||||
IUserGroupService userGroupService,
|
||||
IUserGroupViewModelFactory userGroupViewModelFactory)
|
||||
{
|
||||
_userGroupService = userGroupService;
|
||||
_userGroupViewModelFactory = userGroupViewModelFactory;
|
||||
}
|
||||
|
||||
[HttpPut("{key:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Update(Guid key, UserGroupUpdateModel dataTypeUpdateModel)
|
||||
{
|
||||
IUserGroup? existingUserGroup = await _userGroupService.GetAsync(key);
|
||||
|
||||
if (existingUserGroup is null)
|
||||
{
|
||||
return UserGroupOperationStatusResult(UserGroupOperationStatus.NotFound);
|
||||
}
|
||||
|
||||
Attempt<IUserGroup, UserGroupOperationStatus> userGroupUpdateAttempt = await _userGroupViewModelFactory.UpdateAsync(existingUserGroup, dataTypeUpdateModel);
|
||||
if (userGroupUpdateAttempt.Success is false)
|
||||
{
|
||||
return UserGroupOperationStatusResult(userGroupUpdateAttempt.Status);
|
||||
}
|
||||
|
||||
IUserGroup userGroup = userGroupUpdateAttempt.Result;
|
||||
Attempt<IUserGroup, UserGroupOperationStatus> result = await _userGroupService.UpdateAsync(userGroup, -1);
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: UserGroupOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.UserGroups;
|
||||
|
||||
// TODO: This needs to be an authorized controller.
|
||||
|
||||
[ApiController]
|
||||
[VersionedApiBackOfficeRoute("user-groups")]
|
||||
[ApiExplorerSettings(GroupName = "User Groups")]
|
||||
[ApiVersion("1.0")]
|
||||
public class UserGroupsControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected IActionResult UserGroupOperationStatusResult(UserGroupOperationStatus status) =>
|
||||
status switch
|
||||
{
|
||||
UserGroupOperationStatus.NotFound => NotFound("The user group could not be found"),
|
||||
UserGroupOperationStatus.AlreadyExists => Conflict(new ProblemDetailsBuilder()
|
||||
.WithTitle("User group already exists")
|
||||
.WithDetail("The user group exists already.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.DuplicateAlias => Conflict(new ProblemDetailsBuilder()
|
||||
.WithTitle("Duplicate alias")
|
||||
.WithDetail("A user group already exists with the attempted alias.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.MissingUser => Unauthorized(new ProblemDetailsBuilder()
|
||||
.WithTitle("Missing user")
|
||||
.WithDetail("A performing user was not found when attempting to create the user group.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.IsSystemUserGroup => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("System user group")
|
||||
.WithDetail("The operation is not allowed on a system user group.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.UnauthorizedMissingUserSection => Unauthorized(new ProblemDetailsBuilder()
|
||||
.WithTitle("Unauthorized")
|
||||
.WithDetail("The performing user does not have access to the required section")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.UnauthorizedMissingSections => Unauthorized(new ProblemDetailsBuilder()
|
||||
.WithTitle("Unauthorized section")
|
||||
.WithDetail("The specified allowed section contained a section the performing user doesn't have access to.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.UnauthorizedStartNodes => Unauthorized(new ProblemDetailsBuilder()
|
||||
.WithTitle("Unauthorized start node")
|
||||
.WithDetail("The specified start nodes contained a start node the performing user doesn't have access to.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.UnauthorizedMissingUserGroup => Unauthorized(new ProblemDetailsBuilder()
|
||||
.WithTitle("User not in user group")
|
||||
.WithDetail("The current user is not in the user group")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Cancelled by notification")
|
||||
.WithDetail("A notification handler prevented the language operation.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.DocumentStartNodeKeyNotFound => NotFound(new ProblemDetailsBuilder()
|
||||
.WithTitle("Document start node key not found")
|
||||
.WithDetail("The assigned document start node does not exists.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.MediaStartNodeKeyNotFound => NotFound(new ProblemDetailsBuilder()
|
||||
.WithTitle("Media start node key not found")
|
||||
.WithDetail("The assigned media start node does not exists.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.LanguageNotFound => NotFound(new ProblemDetailsBuilder()
|
||||
.WithTitle("Language not found")
|
||||
.WithDetail("The specified language cannot be found.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.NameTooLong => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Name too long")
|
||||
.WithDetail("User Group name is too long.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.AliasTooLong => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Alias too long")
|
||||
.WithDetail("The user group alias is too long.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.MissingName => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Missing user group name.")
|
||||
.WithDetail("The user group name is required, and cannot be an empty string.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown user group operation status."),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
|
||||
internal static class UserGroupsBuilderExtensions
|
||||
{
|
||||
internal static IUmbracoBuilder AddUserGroups(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddTransient<IUserGroupViewModelFactory, UserGroupViewModelFactory>();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroups;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
/// <summary>
|
||||
/// A factory for creating <see cref="UserGroupViewModel"/>
|
||||
/// </summary>
|
||||
public interface IUserGroupViewModelFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="UserGroupViewModel"/> based on a <see cref="UserGroup"/>
|
||||
/// </summary>
|
||||
/// <param name="userGroup"></param>
|
||||
/// <returns></returns>
|
||||
Task<UserGroupViewModel> CreateAsync(IUserGroup userGroup);
|
||||
|
||||
/// <summary>
|
||||
/// Creates multiple <see cref="UserGroupViewModel"/> base on multiple <see cref="UserGroup"/>
|
||||
/// </summary>
|
||||
/// <param name="userGroups"></param>
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<UserGroupViewModel>> CreateMultipleAsync(IEnumerable<IUserGroup> userGroups);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="IUserGroup"/> based on a <see cref="UserGroupSaveModel"/>
|
||||
/// </summary>
|
||||
/// <param name="saveModel"></param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
Task<Attempt<IUserGroup, UserGroupOperationStatus>> CreateAsync(UserGroupSaveModel saveModel);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the values of an update model to fit with the existing backoffice implementations, and maps it to an existing user group.
|
||||
/// </summary>
|
||||
/// <param name="current">Existing user group to map to.</param>
|
||||
/// <param name="update">Update model containing the new values.</param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
Task<Attempt<IUserGroup, UserGroupOperationStatus>> UpdateAsync(IUserGroup current, UserGroupUpdateModel update);
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
using Umbraco.Cms.Api.Management.Mapping;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroups;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class UserGroupViewModelFactory : IUserGroupViewModelFactory
|
||||
{
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly ILanguageService _languageService;
|
||||
|
||||
public UserGroupViewModelFactory(
|
||||
IEntityService entityService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
ILanguageService languageService)
|
||||
{
|
||||
_entityService = entityService;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_languageService = languageService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UserGroupViewModel> CreateAsync(IUserGroup userGroup)
|
||||
{
|
||||
Guid? contentStartNodeKey = GetKeyFromId(userGroup.StartContentId, UmbracoObjectTypes.Document);
|
||||
Guid? mediaStartNodeKey = GetKeyFromId(userGroup.StartMediaId, UmbracoObjectTypes.Media);
|
||||
Attempt<IEnumerable<string>, UserGroupOperationStatus> languageIsoCodesMappingAttempt = await MapLanguageIdsToIsoCodeAsync(userGroup.AllowedLanguages);
|
||||
|
||||
// We've gotten this data from the database, so the mapping should not fail
|
||||
if (languageIsoCodesMappingAttempt.Success is false)
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown language ID in User Group: {userGroup.Name}");
|
||||
}
|
||||
|
||||
return new UserGroupViewModel
|
||||
{
|
||||
Name = userGroup.Name ?? string.Empty,
|
||||
Key = userGroup.Key,
|
||||
DocumentStartNodeKey = contentStartNodeKey,
|
||||
MediaStartNodeKey = mediaStartNodeKey,
|
||||
Icon = userGroup.Icon,
|
||||
Languages = languageIsoCodesMappingAttempt.Result,
|
||||
HasAccessToAllLanguages = userGroup.HasAccessToAllLanguages,
|
||||
Permissions = userGroup.PermissionNames,
|
||||
Sections = userGroup.AllowedSections.Select(SectionMapper.GetName),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<UserGroupViewModel>> CreateMultipleAsync(IEnumerable<IUserGroup> userGroups)
|
||||
{
|
||||
var userGroupViewModels = new List<UserGroupViewModel>();
|
||||
foreach (IUserGroup userGroup in userGroups)
|
||||
{
|
||||
userGroupViewModels.Add(await CreateAsync(userGroup));
|
||||
}
|
||||
|
||||
return userGroupViewModels;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Attempt<IUserGroup, UserGroupOperationStatus>> CreateAsync(UserGroupSaveModel saveModel)
|
||||
{
|
||||
var cleanedName = saveModel.Name.CleanForXss('[', ']', '(', ')', ':');
|
||||
|
||||
var group = new UserGroup(_shortStringHelper)
|
||||
{
|
||||
Name = cleanedName,
|
||||
Alias = cleanedName,
|
||||
Icon = saveModel.Icon,
|
||||
HasAccessToAllLanguages = saveModel.HasAccessToAllLanguages,
|
||||
PermissionNames = saveModel.Permissions,
|
||||
};
|
||||
|
||||
Attempt<UserGroupOperationStatus> assignmentAttempt = AssignStartNodesToUserGroup(saveModel, group);
|
||||
if (assignmentAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus<IUserGroup, UserGroupOperationStatus>(assignmentAttempt.Result, group);
|
||||
}
|
||||
|
||||
foreach (var section in saveModel.Sections)
|
||||
{
|
||||
group.AddAllowedSection(SectionMapper.GetAlias(section));
|
||||
}
|
||||
|
||||
Attempt<IEnumerable<int>, UserGroupOperationStatus> languageIsoCodeMappingAttempt = await MapLanguageIsoCodesToIdsAsync(saveModel.Languages);
|
||||
if (languageIsoCodeMappingAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus<IUserGroup, UserGroupOperationStatus>(languageIsoCodeMappingAttempt.Status, group);
|
||||
}
|
||||
|
||||
foreach (var languageId in languageIsoCodeMappingAttempt.Result)
|
||||
{
|
||||
group.AddAllowedLanguage(languageId);
|
||||
}
|
||||
|
||||
return Attempt.SucceedWithStatus<IUserGroup, UserGroupOperationStatus>(UserGroupOperationStatus.Success, group);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Attempt<IUserGroup, UserGroupOperationStatus>> UpdateAsync(IUserGroup current, UserGroupUpdateModel update)
|
||||
{
|
||||
Attempt<UserGroupOperationStatus> assignmentAttempt = AssignStartNodesToUserGroup(update, current);
|
||||
if (assignmentAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(assignmentAttempt.Result, current);
|
||||
}
|
||||
|
||||
current.ClearAllowedLanguages();
|
||||
Attempt<IEnumerable<int>, UserGroupOperationStatus> languageIdsMappingAttempt = await MapLanguageIsoCodesToIdsAsync(update.Languages);
|
||||
if (languageIdsMappingAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(languageIdsMappingAttempt.Status, current);
|
||||
}
|
||||
|
||||
foreach (var languageId in languageIdsMappingAttempt.Result)
|
||||
{
|
||||
current.AddAllowedLanguage(languageId);
|
||||
}
|
||||
|
||||
current.ClearAllowedSections();
|
||||
foreach (var sectionName in update.Sections)
|
||||
{
|
||||
current.AddAllowedSection(SectionMapper.GetAlias(sectionName));
|
||||
}
|
||||
|
||||
current.Name = update.Name.CleanForXss('[', ']', '(', ')', ':');
|
||||
current.Icon = update.Icon;
|
||||
current.HasAccessToAllLanguages = update.HasAccessToAllLanguages;
|
||||
current.PermissionNames = update.Permissions;
|
||||
|
||||
|
||||
return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, current);
|
||||
}
|
||||
|
||||
private async Task<Attempt<IEnumerable<string>, UserGroupOperationStatus>> MapLanguageIdsToIsoCodeAsync(IEnumerable<int> ids)
|
||||
{
|
||||
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
|
||||
string[] isoCodes = languages
|
||||
.Where(x => ids.Contains(x.Id))
|
||||
.Select(x => x.IsoCode)
|
||||
.ToArray();
|
||||
|
||||
return isoCodes.Length == ids.Count()
|
||||
? Attempt.SucceedWithStatus<IEnumerable<string>, UserGroupOperationStatus>(UserGroupOperationStatus.Success, isoCodes)
|
||||
: Attempt.FailWithStatus<IEnumerable<string>, UserGroupOperationStatus>(UserGroupOperationStatus.LanguageNotFound, isoCodes);
|
||||
}
|
||||
|
||||
private async Task<Attempt<IEnumerable<int>, UserGroupOperationStatus>> MapLanguageIsoCodesToIdsAsync(IEnumerable<string> isoCodes)
|
||||
{
|
||||
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
|
||||
int[] languageIds = languages
|
||||
.Where(x => isoCodes.Contains(x.IsoCode))
|
||||
.Select(x => x.Id)
|
||||
.ToArray();
|
||||
|
||||
return languageIds.Length == isoCodes.Count()
|
||||
? Attempt.SucceedWithStatus<IEnumerable<int>, UserGroupOperationStatus>(UserGroupOperationStatus.Success, languageIds)
|
||||
: Attempt.FailWithStatus<IEnumerable<int>, UserGroupOperationStatus>(UserGroupOperationStatus.LanguageNotFound, languageIds);
|
||||
}
|
||||
|
||||
private Attempt<UserGroupOperationStatus> AssignStartNodesToUserGroup(UserGroupBase source, IUserGroup target)
|
||||
{
|
||||
if (source.DocumentStartNodeKey is not null)
|
||||
{
|
||||
var contentId = GetIdFromKey(source.DocumentStartNodeKey.Value, UmbracoObjectTypes.Document);
|
||||
|
||||
if (contentId is null)
|
||||
{
|
||||
return Attempt.Fail(UserGroupOperationStatus.DocumentStartNodeKeyNotFound);
|
||||
}
|
||||
|
||||
target.StartContentId = contentId;
|
||||
}
|
||||
|
||||
if (source.MediaStartNodeKey is not null)
|
||||
{
|
||||
var mediaId = GetIdFromKey(source.MediaStartNodeKey.Value, UmbracoObjectTypes.Media);
|
||||
|
||||
if (mediaId is null)
|
||||
{
|
||||
return Attempt.Fail(UserGroupOperationStatus.MediaStartNodeKeyNotFound);
|
||||
}
|
||||
|
||||
target.StartMediaId = mediaId;
|
||||
}
|
||||
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
private Guid? GetKeyFromId(int? id, UmbracoObjectTypes objectType)
|
||||
{
|
||||
if (id is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Attempt<Guid> attempt = _entityService.GetKey(id.Value, objectType);
|
||||
if (attempt.Success is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return attempt.Result;
|
||||
}
|
||||
|
||||
private int? GetIdFromKey(Guid key, UmbracoObjectTypes objectType)
|
||||
{
|
||||
Attempt<int> attempt = _entityService.GetId(key, objectType);
|
||||
|
||||
if (attempt.Success is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return attempt.Result;
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ public class ManagementApiComposer : IComposer
|
||||
.AddDataTypes()
|
||||
.AddTemplates()
|
||||
.AddLogViewer()
|
||||
.AddUserGroups()
|
||||
.AddBackOfficeAuthentication()
|
||||
.AddApiVersioning()
|
||||
.AddSwaggerGen();
|
||||
|
||||
54
src/Umbraco.Cms.Api.Management/Mapping/SectionMapper.cs
Normal file
54
src/Umbraco.Cms.Api.Management/Mapping/SectionMapper.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace Umbraco.Cms.Api.Management.Mapping;
|
||||
|
||||
/// <summary>
|
||||
/// Maps from the old section aliases to the new section names.
|
||||
/// This is static since it's expected to be removed, so might as well make the clean up work as easy as possible.
|
||||
/// FIXME: This is a temporary thing until permissions is fleshed out and section is either migrated to some form of permission
|
||||
/// </summary>
|
||||
public static class SectionMapper
|
||||
{
|
||||
private static readonly List<SectionMapping> _sectionMappings = new()
|
||||
{
|
||||
new SectionMapping { Alias = "content", Name = "Umb.Section.Content" },
|
||||
new SectionMapping { Alias = "media", Name = "Umb.Section.Media" },
|
||||
new SectionMapping { Alias = "member", Name = "Umb.Section.Members" },
|
||||
new SectionMapping { Alias = "settings", Name = "Umb.Section.Settings" },
|
||||
new SectionMapping { Alias = "packages", Name = "Umb.Section.Packages" },
|
||||
new SectionMapping { Alias = "translation", Name = "Umb.Section.Translation" },
|
||||
new SectionMapping { Alias = "users", Name = "Umb.Section.Users" },
|
||||
new SectionMapping { Alias = "forms", Name = "Umb.Section.Forms" },
|
||||
};
|
||||
|
||||
public static string GetName(string alias)
|
||||
{
|
||||
SectionMapping? mapping = _sectionMappings.FirstOrDefault(x => x.Alias == alias);
|
||||
|
||||
if (mapping is not null)
|
||||
{
|
||||
return mapping.Name;
|
||||
}
|
||||
|
||||
// If we can't find it we just fall back to the alias
|
||||
return alias;
|
||||
}
|
||||
|
||||
public static string GetAlias(string name)
|
||||
{
|
||||
SectionMapping? mapping = _sectionMappings.FirstOrDefault(x => x.Name == name);
|
||||
|
||||
if (mapping is not null)
|
||||
{
|
||||
return mapping.Alias;
|
||||
}
|
||||
|
||||
// If we can't find it we just fall back to the name
|
||||
return name;
|
||||
}
|
||||
|
||||
private class SectionMapping
|
||||
{
|
||||
public required string Alias { get; init; }
|
||||
|
||||
public required string Name { get; init; }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
|
||||
public class DataTypeViewModel : DataTypeModelBase
|
||||
public class DataTypeViewModel : DataTypeModelBase, INamedEntityViewModel
|
||||
{
|
||||
public Guid Key { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
|
||||
public class DictionaryItemViewModel : DictionaryItemModelBase
|
||||
public class DictionaryItemViewModel : DictionaryItemModelBase, INamedEntityViewModel
|
||||
{
|
||||
public Guid Key { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Folder;
|
||||
|
||||
public class FolderViewModel : FolderModelBase
|
||||
public class FolderViewModel : FolderModelBase, INamedEntityViewModel
|
||||
{
|
||||
public Guid Key { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels;
|
||||
|
||||
public interface INamedEntityViewModel
|
||||
{
|
||||
Guid Key { get; }
|
||||
|
||||
string Name { get;}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.RecycleBin;
|
||||
|
||||
public class RecycleBinItemViewModel
|
||||
public class RecycleBinItemViewModel : INamedEntityViewModel
|
||||
{
|
||||
public Guid Key { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Template;
|
||||
|
||||
public class TemplateViewModel : TemplateModelBase
|
||||
public class TemplateViewModel : TemplateModelBase, INamedEntityViewModel
|
||||
{
|
||||
public Guid Key { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
|
||||
public class EntityTreeItemViewModel : TreeItemViewModel
|
||||
public class EntityTreeItemViewModel : TreeItemViewModel, INamedEntityViewModel
|
||||
{
|
||||
public Guid Key { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Base class for front-end representation of a User Group.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Contains all the properties shared between Save, Update, Representation, etc...
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class UserGroupBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the user groups
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The Icon for the user group
|
||||
/// </summary>
|
||||
public string? Icon { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The sections that the user group has access to
|
||||
/// </summary>
|
||||
public required IEnumerable<string> Sections { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The languages that the user group has access to
|
||||
/// </summary>
|
||||
public required IEnumerable<string> Languages { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Flag indicating if the user group gives access to all languages, regardless of <see cref="UserGroupBase.Languages"/>.
|
||||
/// </summary>
|
||||
public required bool HasAccessToAllLanguages { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the document that should act as root node for the user group
|
||||
/// <remarks>
|
||||
/// This can be overwritten by a different user group if a user is a member of multiple groups
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public Guid? DocumentStartNodeKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The Key of the media that should act as root node for the user group
|
||||
/// <remarks>
|
||||
/// This can be overwritten by a different user group if a user is a member of multiple groups
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public Guid? MediaStartNodeKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ad-hoc list of permissions provided, and maintained by the front-end. The server has no concept of what these mean.
|
||||
/// </summary>
|
||||
public required ISet<string> Permissions { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups;
|
||||
|
||||
public class UserGroupSaveModel : UserGroupBase
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups;
|
||||
|
||||
public class UserGroupUpdateModel : UserGroupBase
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups;
|
||||
|
||||
public class UserGroupViewModel : UserGroupBase, INamedEntityViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The key identifier for the user group.
|
||||
/// </summary>
|
||||
public required Guid Key { get; init; }
|
||||
|
||||
}
|
||||
@@ -203,6 +203,13 @@
|
||||
<Right>lib/net7.0/Umbraco.Core.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Core.Models.Mapping.UserMapDefinition.#ctor(Umbraco.Cms.Core.Services.ILocalizedTextService,Umbraco.Cms.Core.Services.IUserService,Umbraco.Cms.Core.Services.IEntityService,Umbraco.Cms.Core.Services.ISectionService,Umbraco.Cms.Core.Cache.AppCaches,Umbraco.Cms.Core.Actions.ActionCollection,Microsoft.Extensions.Options.IOptions{Umbraco.Cms.Core.Configuration.Models.GlobalSettings},Umbraco.Cms.Core.IO.MediaFileManager,Umbraco.Cms.Core.Strings.IShortStringHelper,Umbraco.Cms.Core.Media.IImageUrlGenerator)</Target>
|
||||
<Left>lib/net7.0/Umbraco.Core.dll</Left>
|
||||
<Right>lib/net7.0/Umbraco.Core.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Core.Models.Membership.ReadOnlyUserGroup.#ctor(System.Int32,System.String,System.String,System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.Collections.Generic.IEnumerable{System.Int32},System.Collections.Generic.IEnumerable{System.String},System.Collections.Generic.IEnumerable{System.String},System.Boolean)</Target>
|
||||
@@ -602,4 +609,11 @@
|
||||
<Right>lib/net7.0/Umbraco.Core.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0006</DiagnosticId>
|
||||
<Target>P:Umbraco.Cms.Core.Models.Membership.IUserGroup.PermissionNames</Target>
|
||||
<Left>lib/net7.0/Umbraco.Core.dll</Left>
|
||||
<Right>lib/net7.0/Umbraco.Core.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
@@ -279,6 +279,8 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddUnique<IKeyValueService, KeyValueService>();
|
||||
Services.AddUnique<IPublicAccessService, PublicAccessService>();
|
||||
Services.AddUnique<IContentVersionService, ContentVersionService>();
|
||||
Services.AddTransient<IUserGroupAuthorizationService, UserGroupAuthorizationService>();
|
||||
Services.AddUnique<IUserGroupService, UserGroupService>();
|
||||
Services.AddUnique<IUserService, UserService>();
|
||||
Services.AddUnique<ILocalizationService, LocalizationService>();
|
||||
Services.AddUnique<IDictionaryItemService, DictionaryItemService>();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
@@ -30,6 +32,7 @@ public sealed class AuditNotificationsHandler :
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IIpResolver _ipResolver;
|
||||
private readonly IMemberService _memberService;
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public AuditNotificationsHandler(
|
||||
@@ -39,7 +42,8 @@ public sealed class AuditNotificationsHandler :
|
||||
IIpResolver ipResolver,
|
||||
IOptionsMonitor<GlobalSettings> globalSettings,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IMemberService memberService)
|
||||
IMemberService memberService,
|
||||
IUserGroupService userGroupService)
|
||||
{
|
||||
_auditService = auditService;
|
||||
_userService = userService;
|
||||
@@ -47,9 +51,32 @@ public sealed class AuditNotificationsHandler :
|
||||
_ipResolver = ipResolver;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_memberService = memberService;
|
||||
_userGroupService = userGroupService;
|
||||
_globalSettings = globalSettings.CurrentValue;
|
||||
}
|
||||
|
||||
[Obsolete("Use constructor that takes IUserGroupService, scheduled for removal in V15.")]
|
||||
public AuditNotificationsHandler(
|
||||
IAuditService auditService,
|
||||
IUserService userService,
|
||||
IEntityService entityService,
|
||||
IIpResolver ipResolver,
|
||||
IOptionsMonitor<GlobalSettings> globalSettings,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IMemberService memberService)
|
||||
: this(
|
||||
auditService,
|
||||
userService,
|
||||
entityService,
|
||||
ipResolver,
|
||||
globalSettings,
|
||||
backOfficeSecurityAccessor,
|
||||
memberService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserGroupService>()
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
private IUser CurrentPerformingUser
|
||||
{
|
||||
get
|
||||
@@ -95,7 +122,7 @@ public sealed class AuditNotificationsHandler :
|
||||
IEnumerable<EntityPermission> perms = notification.EntityPermissions;
|
||||
foreach (EntityPermission perm in perms)
|
||||
{
|
||||
IUserGroup? group = _userService.GetUserGroupById(perm.UserGroupId);
|
||||
IUserGroup? group = _userGroupService.GetAsync(perm.UserGroupId).Result;
|
||||
var assigned = string.Join(", ", perm.AssignedPermissions ?? Array.Empty<string>());
|
||||
IEntitySlim? entity = _entityService.Get(perm.EntityId);
|
||||
|
||||
|
||||
@@ -37,6 +37,16 @@ public class UserGroupSave : EntityBasic, IValidatableObject
|
||||
[DataMember(Name = "hasAccessToAllLanguages")]
|
||||
public bool HasAccessToAllLanguages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A set of ad-hoc permissions provided by the frontend.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default the server has no concept of what these strings mean, we simple store them and return them to the UI.
|
||||
/// FIXME: Permissions already exists in the form of "DefaultPermissions", but is subject to change in the future
|
||||
/// when we know more about how we want to handle permissions, potentially those will be migrated in the these "soft" permissions.
|
||||
/// </remarks>
|
||||
public ISet<string>? Permissions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of letters (permission codes) to assign as the default for the user group
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
@@ -33,6 +32,7 @@ public class UserMapDefinition : IMapDefinition
|
||||
private readonly ILocalizedTextService _textService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
|
||||
public UserMapDefinition(
|
||||
ILocalizedTextService textService,
|
||||
@@ -45,7 +45,8 @@ public class UserMapDefinition : IMapDefinition
|
||||
MediaFileManager mediaFileManager,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IImageUrlGenerator imageUrlGenerator,
|
||||
ILocalizationService localizationService)
|
||||
ILocalizationService localizationService,
|
||||
IUserGroupService userGroupService)
|
||||
{
|
||||
_sectionService = sectionService;
|
||||
_entityService = entityService;
|
||||
@@ -58,9 +59,10 @@ public class UserMapDefinition : IMapDefinition
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_imageUrlGenerator = imageUrlGenerator;
|
||||
_localizationService = localizationService;
|
||||
_userGroupService = userGroupService;
|
||||
}
|
||||
|
||||
[Obsolete("Please use constructor that takes an ILocalizationService instead")]
|
||||
[Obsolete("Use constructor that takes IUserGroupService, scheduled for removal in V15.")]
|
||||
public UserMapDefinition(
|
||||
ILocalizedTextService textService,
|
||||
IUserService userService,
|
||||
@@ -71,19 +73,21 @@ public class UserMapDefinition : IMapDefinition
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
MediaFileManager mediaFileManager,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IImageUrlGenerator imageUrlGenerator)
|
||||
: this(
|
||||
textService,
|
||||
userService,
|
||||
entityService,
|
||||
sectionService,
|
||||
appCaches,
|
||||
actions,
|
||||
globalSettings,
|
||||
mediaFileManager,
|
||||
shortStringHelper,
|
||||
imageUrlGenerator,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILocalizationService>())
|
||||
IImageUrlGenerator imageUrlGenerator,
|
||||
ILocalizationService localizationService)
|
||||
: this(
|
||||
textService,
|
||||
userService,
|
||||
entityService,
|
||||
sectionService,
|
||||
appCaches,
|
||||
actions,
|
||||
globalSettings,
|
||||
mediaFileManager,
|
||||
shortStringHelper,
|
||||
imageUrlGenerator,
|
||||
localizationService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserGroupService>())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -135,6 +139,7 @@ public class UserMapDefinition : IMapDefinition
|
||||
target.Permissions = source.DefaultPermissions;
|
||||
target.Key = source.Key;
|
||||
target.HasAccessToAllLanguages = source.HasAccessToAllLanguages;
|
||||
target.PermissionNames = source.Permissions ?? new HashSet<string>();
|
||||
|
||||
var id = GetIntId(source.Id);
|
||||
if (id > 0)
|
||||
@@ -222,7 +227,7 @@ public class UserMapDefinition : IMapDefinition
|
||||
target.IsApproved = false;
|
||||
|
||||
target.ClearGroups();
|
||||
IEnumerable<IUserGroup> groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray());
|
||||
IEnumerable<IUserGroup> groups = _userGroupService.GetAsync(source.UserGroups.ToArray()).GetAwaiter().GetResult();
|
||||
foreach (IUserGroup group in groups)
|
||||
{
|
||||
target.AddGroup(group.ToReadOnlyGroup());
|
||||
@@ -247,7 +252,7 @@ public class UserMapDefinition : IMapDefinition
|
||||
target.Id = source.Id;
|
||||
|
||||
target.ClearGroups();
|
||||
IEnumerable<IUserGroup> groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray());
|
||||
IEnumerable<IUserGroup> groups = _userGroupService.GetAsync(source.UserGroups.ToArray()).GetAwaiter().GetResult();
|
||||
foreach (IUserGroup group in groups)
|
||||
{
|
||||
target.AddGroup(group.ToReadOnlyGroup());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models.Membership;
|
||||
@@ -24,7 +25,11 @@ public interface IUserGroup : IEntity, IRememberBeingDirty
|
||||
/// If this property is true it will give the group access to all languages
|
||||
/// </summary>
|
||||
/// This is set to return true as default to avoid breaking changes
|
||||
public bool HasAccessToAllLanguages => true;
|
||||
public bool HasAccessToAllLanguages
|
||||
{
|
||||
get => true;
|
||||
set { /* This is NoOp to avoid breaking changes */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The set of default permissions
|
||||
@@ -35,6 +40,16 @@ public interface IUserGroup : IEntity, IRememberBeingDirty
|
||||
/// </remarks>
|
||||
IEnumerable<string>? Permissions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The set of permissions provided by the frontend.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default the server has no concept of what these strings mean, we simple store them and return them to the UI.
|
||||
/// FIXME: For now this is named PermissionNames since Permissions already exists, but is subject to change in the future
|
||||
/// when we know more about how we want to handle permissions, potentially those will be migrated in the these "soft" permissions.
|
||||
/// </remarks>
|
||||
ISet<string> PermissionNames { get; set; }
|
||||
|
||||
IEnumerable<string> AllowedSections { get; }
|
||||
|
||||
void RemoveAllowedSection(string sectionAlias);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
@@ -24,6 +25,7 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
|
||||
private string _name;
|
||||
private bool _hasAccessToAllLanguages;
|
||||
private IEnumerable<string>? _permissions;
|
||||
private ISet<string> _permissionNames = new HashSet<string>();
|
||||
private List<string> _sectionCollection;
|
||||
private List<int> _languageCollection;
|
||||
private int? _startContentId;
|
||||
@@ -124,6 +126,13 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _permissions, nameof(Permissions), _stringEnumerableComparer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISet<string> PermissionNames
|
||||
{
|
||||
get => _permissionNames;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _permissionNames!, nameof(PermissionNames), _stringEnumerableComparer);
|
||||
}
|
||||
|
||||
public IEnumerable<string> AllowedSections => _sectionCollection;
|
||||
|
||||
public int UserCount { get; }
|
||||
|
||||
@@ -54,6 +54,7 @@ public static partial class Constants
|
||||
public const string User2NodeNotify = TableNamePrefix + "User2NodeNotify";
|
||||
public const string UserGroup2App = TableNamePrefix + "UserGroup2App";
|
||||
public const string UserGroup2Node = TableNamePrefix + "UserGroup2Node";
|
||||
public const string UserGroup2Permission = TableNamePrefix + "UserGroup2Permission";
|
||||
public const string UserGroup2NodePermission = TableNamePrefix + "UserGroup2NodePermission";
|
||||
public const string UserGroup2Language = TableNamePrefix + "UserGroup2Language";
|
||||
public const string ExternalLogin = TableNamePrefix + "ExternalLogin";
|
||||
|
||||
@@ -48,6 +48,22 @@ public interface IEntityRepository : IRepository
|
||||
|
||||
bool Exists(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Asserts if an entity with the given object type exists.
|
||||
/// </summary>
|
||||
/// <param name="key">The Key of the entity to find.</param>
|
||||
/// <param name="objectType">The object type key of the entity.</param>
|
||||
/// <returns>True if an entity with the given key and object type exists.</returns>
|
||||
bool Exists(Guid key, Guid objectType) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Asserts if an entity with the given object type exists.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the entity to find.</param>
|
||||
/// <param name="objectType">The object type key of the entity.</param>
|
||||
/// <returns>True if an entity with the given id and object type exists.</returns>
|
||||
bool Exists(int id, Guid objectType) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Gets paged entities for a query and a specific object type
|
||||
/// </summary>
|
||||
|
||||
@@ -124,6 +124,24 @@ public class EntityService : RepositoryService, IEntityService
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Exists(Guid key, UmbracoObjectTypes objectType)
|
||||
{
|
||||
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
return _entityRepository.Exists(key, objectType.GetGuid());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Exists(int id, UmbracoObjectTypes objectType)
|
||||
{
|
||||
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
return _entityRepository.Exists(id, objectType.GetGuid());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<IEntitySlim> GetAll<T>()
|
||||
where T : IUmbracoEntity
|
||||
|
||||
@@ -60,6 +60,22 @@ public interface IEntityService
|
||||
/// <param name="key">The unique key of the entity.</param>
|
||||
bool Exists(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether and entity of a certain object type exists.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key of the entity.</param>
|
||||
/// <param name="objectType">The object type to look for.</param>
|
||||
/// <returns>True if the entity exists, false if it does not.</returns>
|
||||
bool Exists(Guid key, UmbracoObjectTypes objectType) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether and entity of a certain object type exists.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the entity.</param>
|
||||
/// <param name="objectType">The object type to look for.</param>
|
||||
/// <returns>True if the entity exists, false if it does not.</returns>
|
||||
bool Exists(int id, UmbracoObjectTypes objectType) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Gets entities of a given object type.
|
||||
/// </summary>
|
||||
|
||||
24
src/Umbraco.Core/Services/IUserGroupAuthorizationService.cs
Normal file
24
src/Umbraco.Core/Services/IUserGroupAuthorizationService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface IUserGroupAuthorizationService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Authorizes a user to create a new user group.
|
||||
/// </summary>
|
||||
/// <param name="performingUser">The user performing the create operation.</param>
|
||||
/// <param name="userGroup">The user group to be created.</param>
|
||||
/// <returns>An attempt with an operation status.</returns>
|
||||
Attempt<UserGroupOperationStatus> AuthorizeUserGroupCreation(IUser performingUser, IUserGroup userGroup);
|
||||
|
||||
/// <summary>
|
||||
/// Authorizes a user to update an existing user group.
|
||||
/// </summary>
|
||||
/// <param name="performingUser">The user performing the update operation.</param>
|
||||
/// <param name="userGroup">The user group to be created.</param>
|
||||
/// <returns>An attempt with an operation.</returns>
|
||||
Attempt<UserGroupOperationStatus> AuthorizeUserGroupUpdate(IUser performingUser, IUserGroup userGroup);
|
||||
}
|
||||
86
src/Umbraco.Core/Services/IUserGroupService.cs
Normal file
86
src/Umbraco.Core/Services/IUserGroupService.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages user groups.
|
||||
/// </summary>
|
||||
public interface IUserGroupService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all user groups.
|
||||
/// </summary>
|
||||
/// <param name="skip">The amount of user groups to skip.</param>
|
||||
/// <param name="take">The amount of user groups to take.</param>
|
||||
/// <returns>All user groups as an enumerable list of <see cref="IUserGroup"/>.</returns>
|
||||
Task<PagedModel<IUserGroup>> GetAllAsync(int skip, int take);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all UserGroups matching an ID in the parameter list.
|
||||
/// </summary>
|
||||
/// <param name="ids">Optional Ids of UserGroups to retrieve.</param>
|
||||
/// <returns>An enumerable list of <see cref="IUserGroup"/>.</returns>
|
||||
Task<IEnumerable<IUserGroup>> GetAsync(params int[] ids);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all UserGroups matching an alias in the parameter list.
|
||||
/// </summary>
|
||||
/// <param name="aliases">Alias of the UserGroup to retrieve.</param>
|
||||
/// <returns>
|
||||
/// <returns>An enumerable list of <see cref="IUserGroup"/>.</returns>
|
||||
/// </returns>
|
||||
Task<IEnumerable<IUserGroup>> GetAsync(params string[] aliases);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a UserGroup by its Alias
|
||||
/// </summary>
|
||||
/// <param name="alias">Name of the UserGroup to retrieve.</param>
|
||||
/// <returns>
|
||||
/// <see cref="IUserGroup" />
|
||||
/// </returns>
|
||||
Task<IUserGroup?> GetAsync(string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a UserGroup by its Id
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the UserGroup to retrieve.</param>
|
||||
/// <returns>
|
||||
/// <see cref="IUserGroup" />
|
||||
/// </returns>
|
||||
Task<IUserGroup?> GetAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a UserGroup by its key
|
||||
/// </summary>
|
||||
/// <param name="key">Key of the UserGroup to retrieve.</param>
|
||||
/// <returns>
|
||||
/// <see cref="IUserGroup" />
|
||||
/// </returns>
|
||||
Task<IUserGroup?> GetAsync(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Persists a new user group.
|
||||
/// </summary>
|
||||
/// <param name="userGroup">The user group to create.</param>
|
||||
/// <param name="performingUserId">The ID of the user responsible for creating the group.</param>
|
||||
/// <param name="groupMembersUserIds">The IDs of the users that should be part of the group when created.</param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
Task<Attempt<IUserGroup, UserGroupOperationStatus>> CreateAsync(IUserGroup userGroup, int performingUserId, int[]? groupMembersUserIds = null);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing user group.
|
||||
/// </summary>
|
||||
/// <param name="userGroup">The user group to update.</param>
|
||||
/// <param name="performingUserId">The ID of the user responsible for updating the group.</param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
Task<Attempt<IUserGroup, UserGroupOperationStatus>> UpdateAsync(IUserGroup userGroup, int performingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a UserGroup
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the user group to delete.</param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
Task<Attempt<UserGroupOperationStatus>> DeleteAsync(Guid key);
|
||||
}
|
||||
@@ -240,6 +240,7 @@ public interface IUserService : IMembershipUserService
|
||||
/// </summary>
|
||||
/// <param name="ids">Optional Ids of UserGroups to retrieve</param>
|
||||
/// <returns>An enumerable list of <see cref="IUserGroup" /></returns>
|
||||
[Obsolete("Use IUserGroupService.GetAsync instead, scheduled for removal in V15.")]
|
||||
IEnumerable<IUserGroup> GetAllUserGroups(params int[] ids);
|
||||
|
||||
/// <summary>
|
||||
@@ -249,6 +250,7 @@ public interface IUserService : IMembershipUserService
|
||||
/// <returns>
|
||||
/// <see cref="IUserGroup" />
|
||||
/// </returns>
|
||||
[Obsolete("Use IUserGroupService.GetAsync instead, scheduled for removal in V15.")]
|
||||
IEnumerable<IUserGroup> GetUserGroupsByAlias(params string[] alias);
|
||||
|
||||
/// <summary>
|
||||
@@ -258,6 +260,7 @@ public interface IUserService : IMembershipUserService
|
||||
/// <returns>
|
||||
/// <see cref="IUserGroup" />
|
||||
/// </returns>
|
||||
[Obsolete("Use IUserGroupService.GetAsync instead, scheduled for removal in V15.")]
|
||||
IUserGroup? GetUserGroupByAlias(string name);
|
||||
|
||||
/// <summary>
|
||||
@@ -267,6 +270,7 @@ public interface IUserService : IMembershipUserService
|
||||
/// <returns>
|
||||
/// <see cref="IUserGroup" />
|
||||
/// </returns>
|
||||
[Obsolete("Use IUserGroupService.GetAsync instead, scheduled for removal in V15.")]
|
||||
IUserGroup? GetUserGroupById(int id);
|
||||
|
||||
/// <summary>
|
||||
@@ -277,12 +281,14 @@ public interface IUserService : IMembershipUserService
|
||||
/// If null than no changes are made to the users who are assigned to this group, however if a value is passed in
|
||||
/// than all users will be removed from this group and only these users will be added
|
||||
/// </param>
|
||||
[Obsolete("Use IUserGroupService.CreateAsync and IUserGroupService.UpdateAsync instead, scheduled for removal in V15.")]
|
||||
void Save(IUserGroup userGroup, int[]? userIds = null);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a UserGroup
|
||||
/// </summary>
|
||||
/// <param name="userGroup">UserGroup to delete</param>
|
||||
[Obsolete("Use IUserGroupService.DeleteAsync instead, scheduled for removal in V15.")]
|
||||
void DeleteUserGroup(IUserGroup userGroup);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
public enum UserGroupOperationStatus
|
||||
{
|
||||
Success,
|
||||
NotFound,
|
||||
AlreadyExists,
|
||||
DuplicateAlias,
|
||||
MissingUser,
|
||||
IsSystemUserGroup,
|
||||
UnauthorizedMissingUserSection,
|
||||
UnauthorizedMissingSections,
|
||||
UnauthorizedStartNodes,
|
||||
UnauthorizedMissingUserGroup,
|
||||
CancelledByNotification,
|
||||
MediaStartNodeKeyNotFound,
|
||||
DocumentStartNodeKeyNotFound,
|
||||
LanguageNotFound,
|
||||
NameTooLong,
|
||||
AliasTooLong,
|
||||
MissingName,
|
||||
}
|
||||
177
src/Umbraco.Core/Services/UserGroupAuthorizationService.cs
Normal file
177
src/Umbraco.Core/Services/UserGroupAuthorizationService.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
internal sealed class UserGroupAuthorizationService : IUserGroupAuthorizationService
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly AppCaches _appCaches;
|
||||
|
||||
public UserGroupAuthorizationService(
|
||||
IContentService contentService,
|
||||
IMediaService mediaService,
|
||||
IEntityService entityService,
|
||||
AppCaches appCaches)
|
||||
{
|
||||
_contentService = contentService;
|
||||
_mediaService = mediaService;
|
||||
_entityService = entityService;
|
||||
_appCaches = appCaches;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Attempt<UserGroupOperationStatus> AuthorizeUserGroupCreation(IUser performingUser, IUserGroup userGroup)
|
||||
{
|
||||
Attempt<UserGroupOperationStatus> hasSectionAccess = AuthorizeHasAccessToUserSection(performingUser);
|
||||
if (hasSectionAccess.Success is false)
|
||||
{
|
||||
return Attempt.Fail(hasSectionAccess.Result);
|
||||
}
|
||||
|
||||
Attempt<UserGroupOperationStatus> authorizeSectionChanges = AuthorizeSectionAccess(performingUser, userGroup);
|
||||
if (authorizeSectionChanges.Success is false)
|
||||
{
|
||||
return Attempt.Fail(authorizeSectionChanges.Result);
|
||||
}
|
||||
|
||||
Attempt<UserGroupOperationStatus> authorizeContentNodeChanges = AuthorizeStartNodeChanges(performingUser, userGroup);
|
||||
return authorizeSectionChanges.Success is false
|
||||
? Attempt.Fail(authorizeContentNodeChanges.Result)
|
||||
: Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Attempt<UserGroupOperationStatus> AuthorizeUserGroupUpdate(IUser performingUser, IUserGroup userGroup)
|
||||
{
|
||||
Attempt<UserGroupOperationStatus> hasAccessToUserSection = AuthorizeHasAccessToUserSection(performingUser);
|
||||
if (hasAccessToUserSection.Success is false)
|
||||
{
|
||||
return Attempt.Fail(hasAccessToUserSection.Result);
|
||||
}
|
||||
|
||||
Attempt<UserGroupOperationStatus> authorizeSectionAccess = AuthorizeSectionAccess(performingUser, userGroup);
|
||||
if (authorizeSectionAccess.Success is false)
|
||||
{
|
||||
return Attempt.Fail(authorizeSectionAccess.Result);
|
||||
}
|
||||
|
||||
Attempt<UserGroupOperationStatus> authorizeGroupAccess = AuthorizeGroupAccess(performingUser, userGroup);
|
||||
if (authorizeGroupAccess.Success is false)
|
||||
{
|
||||
return Attempt.Fail(authorizeGroupAccess.Result);
|
||||
}
|
||||
|
||||
Attempt<UserGroupOperationStatus> authorizeStartNodeChanges = AuthorizeStartNodeChanges(performingUser, userGroup);
|
||||
if (authorizeSectionAccess.Success is false)
|
||||
{
|
||||
return Attempt.Fail(authorizeStartNodeChanges.Result);
|
||||
}
|
||||
|
||||
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authorize that a user is not adding a section to the group that they don't have access to.
|
||||
/// </summary>
|
||||
/// <param name="performingUser">The user performing the action.</param>
|
||||
/// <param name="userGroup">The UserGroup being created or updated.</param>
|
||||
/// <returns>An attempt with an operation status.</returns>
|
||||
private Attempt<UserGroupOperationStatus> AuthorizeSectionAccess(IUser performingUser, IUserGroup userGroup)
|
||||
{
|
||||
if (performingUser.IsAdmin())
|
||||
{
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
IEnumerable<string> sectionsMissingAccess = userGroup.AllowedSections.Except(performingUser.AllowedSections).ToArray();
|
||||
return sectionsMissingAccess.Any()
|
||||
? Attempt.Fail(UserGroupOperationStatus.UnauthorizedMissingSections)
|
||||
: Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authorize that the user is not changing to a start node that they don't have access to.
|
||||
/// </summary>
|
||||
/// <param name="user">The user performing the action.</param>
|
||||
/// <param name="userGroup">The UserGroup being created or updated.</param>
|
||||
/// <returns>An attempt with an operation status.</returns>
|
||||
private Attempt<UserGroupOperationStatus> AuthorizeStartNodeChanges(IUser user, IUserGroup userGroup)
|
||||
{
|
||||
Attempt<UserGroupOperationStatus> authorizeContent = AuthorizeContentStartNode(user, userGroup);
|
||||
|
||||
return authorizeContent.Success is false
|
||||
? authorizeContent
|
||||
: AuthorizeMediaStartNode(user, userGroup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that a user has access to the user section.
|
||||
/// </summary>
|
||||
/// <param name="user">The user performing the action.</param>
|
||||
/// <returns>An attempt with an operation status.</returns>
|
||||
private Attempt<UserGroupOperationStatus> AuthorizeHasAccessToUserSection(IUser user)
|
||||
{
|
||||
if (user.AllowedSections.Contains(Constants.Applications.Users) is false)
|
||||
{
|
||||
return Attempt.Fail(UserGroupOperationStatus.UnauthorizedMissingUserSection);
|
||||
}
|
||||
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the performing user is part of the user group.
|
||||
/// </summary>
|
||||
private Attempt<UserGroupOperationStatus> AuthorizeGroupAccess(IUser performingUser, IUserGroup userGroup) =>
|
||||
performingUser.Groups.Any(x => x.Key == userGroup.Key) || performingUser.IsAdmin()
|
||||
? Attempt.Succeed(UserGroupOperationStatus.Success)
|
||||
: Attempt.Fail(UserGroupOperationStatus.UnauthorizedMissingUserGroup);
|
||||
|
||||
// We explicitly take an IUser here which is non-nullable, since nullability should be handled in caller.
|
||||
private Attempt<UserGroupOperationStatus> AuthorizeContentStartNode(IUser user, IUserGroup userGroup)
|
||||
{
|
||||
if (userGroup.StartContentId is null)
|
||||
{
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
IContent? content = _contentService.GetById(userGroup.StartContentId.Value);
|
||||
|
||||
if (content is null)
|
||||
{
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
return user.HasPathAccess(content, _entityService, _appCaches) is false
|
||||
? Attempt.Fail(UserGroupOperationStatus.UnauthorizedStartNodes)
|
||||
: Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
// We explicitly take an IUser here which is non-nullable, since nullability should be handled in caller.
|
||||
private Attempt<UserGroupOperationStatus> AuthorizeMediaStartNode(IUser user, IUserGroup userGroup)
|
||||
{
|
||||
|
||||
if (userGroup.StartMediaId is null)
|
||||
{
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
IMedia? media = _mediaService.GetById(userGroup.StartMediaId.Value);
|
||||
|
||||
if (media is null)
|
||||
{
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
return user.HasPathAccess(media, _entityService, _appCaches) is false
|
||||
? Attempt.Fail(UserGroupOperationStatus.UnauthorizedStartNodes)
|
||||
: Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
}
|
||||
376
src/Umbraco.Core/Services/UserGroupService.cs
Normal file
376
src/Umbraco.Core/Services/UserGroupService.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
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.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
/// <inheritdoc cref="Umbraco.Cms.Core.Services.IUserGroupService" />
|
||||
internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
{
|
||||
public const int MaxUserGroupNameLength = 200;
|
||||
public const int MaxUserGroupAliasLength = 200;
|
||||
|
||||
private readonly IUserGroupRepository _userGroupRepository;
|
||||
private readonly IUserGroupAuthorizationService _userGroupAuthorizationService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEntityService _entityService;
|
||||
|
||||
public UserGroupService(
|
||||
ICoreScopeProvider provider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IUserGroupRepository userGroupRepository,
|
||||
IUserGroupAuthorizationService userGroupAuthorizationService,
|
||||
IUserService userService,
|
||||
IEntityService entityService)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
{
|
||||
_userGroupRepository = userGroupRepository;
|
||||
_userGroupAuthorizationService = userGroupAuthorizationService;
|
||||
_userService = userService;
|
||||
_entityService = entityService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<PagedModel<IUserGroup>> GetAllAsync(int skip, int take)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
IUserGroup[] groups = _userGroupRepository.GetMany()
|
||||
.OrderBy(x => x.Name).ToArray();
|
||||
|
||||
var total = groups.Length;
|
||||
|
||||
return Task.FromResult(new PagedModel<IUserGroup>
|
||||
{
|
||||
Items = groups.Skip(skip).Take(take),
|
||||
Total = total,
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IEnumerable<IUserGroup>> GetAsync(params int[] ids)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
IEnumerable<IUserGroup> groups = _userGroupRepository
|
||||
.GetMany(ids)
|
||||
.OrderBy(x => x.Name);
|
||||
|
||||
return Task.FromResult(groups);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IEnumerable<IUserGroup>> GetAsync(params string[] aliases)
|
||||
{
|
||||
if (aliases.Length == 0)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<IUserGroup>());
|
||||
}
|
||||
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
|
||||
IQuery<IUserGroup> query = Query<IUserGroup>().Where(x => aliases.SqlIn(x.Alias));
|
||||
IEnumerable<IUserGroup> contents = _userGroupRepository
|
||||
.Get(query)
|
||||
.WhereNotNull()
|
||||
.OrderBy(x => x.Name)
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult(contents);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IUserGroup?> GetAsync(string alias)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(alias))
|
||||
{
|
||||
throw new ArgumentException("Value cannot be null or whitespace.", nameof(alias));
|
||||
}
|
||||
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
|
||||
IQuery<IUserGroup> query = Query<IUserGroup>().Where(x => x.Alias == alias);
|
||||
IUserGroup? contents = _userGroupRepository.Get(query).FirstOrDefault();
|
||||
return Task.FromResult(contents);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IUserGroup?> GetAsync(int id)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
return Task.FromResult(_userGroupRepository.Get(id));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IUserGroup?> GetAsync(Guid key)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
|
||||
IQuery<IUserGroup> query = Query<IUserGroup>().Where(x => x.Key == key);
|
||||
IUserGroup? groups = _userGroupRepository.Get(query).FirstOrDefault();
|
||||
return Task.FromResult(groups);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Attempt<UserGroupOperationStatus>> DeleteAsync(Guid key)
|
||||
{
|
||||
IUserGroup? userGroup = await GetAsync(key);
|
||||
|
||||
Attempt<UserGroupOperationStatus> validationResult = ValidateUserGroupDeletion(userGroup);
|
||||
if (validationResult.Success is false)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
||||
{
|
||||
var deletingNotification = new UserGroupDeletingNotification(userGroup!, eventMessages);
|
||||
|
||||
if (await scope.Notifications.PublishCancelableAsync(deletingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.Fail(UserGroupOperationStatus.CancelledByNotification);
|
||||
}
|
||||
|
||||
_userGroupRepository.Delete(userGroup!);
|
||||
|
||||
scope.Notifications.Publish(new UserGroupDeletedNotification(userGroup!, eventMessages).WithStateFrom(deletingNotification));
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
private Attempt<UserGroupOperationStatus> ValidateUserGroupDeletion(IUserGroup? userGroup)
|
||||
{
|
||||
if (userGroup is null)
|
||||
{
|
||||
return Attempt.Fail(UserGroupOperationStatus.NotFound);
|
||||
}
|
||||
|
||||
if (userGroup.IsSystemUserGroup())
|
||||
{
|
||||
return Attempt.Fail(UserGroupOperationStatus.IsSystemUserGroup);
|
||||
}
|
||||
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Attempt<IUserGroup, UserGroupOperationStatus>> CreateAsync(
|
||||
IUserGroup userGroup,
|
||||
int performingUserId,
|
||||
int[]? groupMembersUserIds = null)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
|
||||
IUser? performingUser = _userService.GetUserById(performingUserId);
|
||||
if (performingUser is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(UserGroupOperationStatus.MissingUser, userGroup);
|
||||
}
|
||||
|
||||
Attempt<IUserGroup, UserGroupOperationStatus> validationAttempt = await ValidateUserGroupCreationAsync(userGroup);
|
||||
if (validationAttempt.Success is false)
|
||||
{
|
||||
return validationAttempt;
|
||||
}
|
||||
|
||||
Attempt<UserGroupOperationStatus> authorizationAttempt = _userGroupAuthorizationService.AuthorizeUserGroupCreation(performingUser, userGroup);
|
||||
if (authorizationAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(authorizationAttempt.Result, userGroup);
|
||||
}
|
||||
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
var savingNotification = new UserGroupSavingNotification(userGroup, eventMessages);
|
||||
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.FailWithStatus(UserGroupOperationStatus.CancelledByNotification, userGroup);
|
||||
}
|
||||
|
||||
var checkedGroupMembers = EnsureNonAdminUserIsInSavedUserGroup(performingUser, groupMembersUserIds ?? Enumerable.Empty<int>()).ToArray();
|
||||
IEnumerable<IUser> usersToAdd = _userService.GetUsersById(checkedGroupMembers);
|
||||
|
||||
// Since this is a brand new creation we don't have to be worried about what users were added and removed
|
||||
// simply put all members that are requested to be in the group will be "added"
|
||||
var userGroupWithUsers = new UserGroupWithUsers(userGroup, usersToAdd.ToArray(), Array.Empty<IUser>());
|
||||
var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, eventMessages);
|
||||
if (await scope.Notifications.PublishCancelableAsync(savingUserGroupWithUsersNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.FailWithStatus(UserGroupOperationStatus.CancelledByNotification, userGroup);
|
||||
}
|
||||
|
||||
_userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, checkedGroupMembers);
|
||||
|
||||
scope.Complete();
|
||||
return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, userGroup);
|
||||
}
|
||||
|
||||
private async Task<Attempt<IUserGroup, UserGroupOperationStatus>> ValidateUserGroupCreationAsync(IUserGroup userGroup)
|
||||
{
|
||||
if (await IsNewUserGroup(userGroup) is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(UserGroupOperationStatus.AlreadyExists, userGroup);
|
||||
}
|
||||
|
||||
UserGroupOperationStatus commonValidationStatus = ValidateCommon(userGroup);
|
||||
if (commonValidationStatus != UserGroupOperationStatus.Success)
|
||||
{
|
||||
return Attempt.FailWithStatus(commonValidationStatus, userGroup);
|
||||
}
|
||||
|
||||
return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, userGroup);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Attempt<IUserGroup, UserGroupOperationStatus>> UpdateAsync(
|
||||
IUserGroup userGroup,
|
||||
int performingUserId)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
|
||||
IUser? performingUser = _userService.GetUserById(performingUserId);
|
||||
if (performingUser is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(UserGroupOperationStatus.MissingUser, userGroup);
|
||||
}
|
||||
|
||||
UserGroupOperationStatus validationStatus = await ValidateUserGroupUpdateAsync(userGroup);
|
||||
if (validationStatus is not UserGroupOperationStatus.Success)
|
||||
{
|
||||
return Attempt.FailWithStatus(validationStatus, userGroup);
|
||||
}
|
||||
|
||||
Attempt<UserGroupOperationStatus> authorizationAttempt = _userGroupAuthorizationService.AuthorizeUserGroupUpdate(performingUser, userGroup);
|
||||
if (authorizationAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(authorizationAttempt.Result, userGroup);
|
||||
}
|
||||
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
var savingNotification = new UserGroupSavingNotification(userGroup, eventMessages);
|
||||
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.FailWithStatus(UserGroupOperationStatus.CancelledByNotification, userGroup);
|
||||
}
|
||||
|
||||
_userGroupRepository.Save(userGroup);
|
||||
scope.Notifications.Publish(new UserGroupSavedNotification(userGroup, eventMessages).WithStateFrom(savingNotification));
|
||||
|
||||
scope.Complete();
|
||||
return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, userGroup);
|
||||
}
|
||||
|
||||
private async Task<UserGroupOperationStatus> ValidateUserGroupUpdateAsync(IUserGroup userGroup)
|
||||
{
|
||||
UserGroupOperationStatus commonValidationStatus = ValidateCommon(userGroup);
|
||||
if (commonValidationStatus != UserGroupOperationStatus.Success)
|
||||
{
|
||||
return commonValidationStatus;
|
||||
}
|
||||
|
||||
if (await IsNewUserGroup(userGroup))
|
||||
{
|
||||
return UserGroupOperationStatus.NotFound;
|
||||
}
|
||||
|
||||
return UserGroupOperationStatus.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate common user group properties, that are shared between update, create, etc.
|
||||
/// </summary>
|
||||
private UserGroupOperationStatus ValidateCommon(IUserGroup userGroup)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userGroup.Name))
|
||||
{
|
||||
return UserGroupOperationStatus.MissingName;
|
||||
}
|
||||
|
||||
if (userGroup.Name.Length > MaxUserGroupNameLength)
|
||||
{
|
||||
return UserGroupOperationStatus.NameTooLong;
|
||||
}
|
||||
|
||||
if (userGroup.Alias.Length > MaxUserGroupAliasLength)
|
||||
{
|
||||
return UserGroupOperationStatus.AliasTooLong;
|
||||
}
|
||||
|
||||
if (UserGroupHasUniqueAlias(userGroup) is false)
|
||||
{
|
||||
return UserGroupOperationStatus.DuplicateAlias;
|
||||
}
|
||||
|
||||
UserGroupOperationStatus startNodesValidationStatus = ValidateStartNodesExists(userGroup);
|
||||
if (startNodesValidationStatus is not UserGroupOperationStatus.Success)
|
||||
{
|
||||
return startNodesValidationStatus;
|
||||
}
|
||||
|
||||
return UserGroupOperationStatus.Success;
|
||||
}
|
||||
|
||||
private async Task<bool> IsNewUserGroup(IUserGroup userGroup)
|
||||
{
|
||||
if (userGroup.Id != 0 && userGroup.HasIdentity is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await GetAsync(userGroup.Key) is null;
|
||||
}
|
||||
|
||||
private UserGroupOperationStatus ValidateStartNodesExists(IUserGroup userGroup)
|
||||
{
|
||||
if (userGroup.StartContentId is not null
|
||||
&& _entityService.Exists(userGroup.StartContentId.Value, UmbracoObjectTypes.Document) is false)
|
||||
{
|
||||
return UserGroupOperationStatus.DocumentStartNodeKeyNotFound;
|
||||
}
|
||||
|
||||
if (userGroup.StartMediaId is not null
|
||||
&& _entityService.Exists(userGroup.StartMediaId.Value, UmbracoObjectTypes.Media) is false)
|
||||
{
|
||||
return UserGroupOperationStatus.MediaStartNodeKeyNotFound;
|
||||
}
|
||||
|
||||
return UserGroupOperationStatus.Success;
|
||||
}
|
||||
|
||||
private bool UserGroupHasUniqueAlias(IUserGroup userGroup) => _userGroupRepository.Get(userGroup.Alias) is null;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the user creating the user group is either an admin, or in the group itself.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is to ensure that the user can access the group they themselves created at a later point and modify it.
|
||||
/// </remarks>
|
||||
private IEnumerable<int> EnsureNonAdminUserIsInSavedUserGroup(IUser performingUser, IEnumerable<int> groupMembersUserIds)
|
||||
{
|
||||
var userIds = groupMembersUserIds.ToList();
|
||||
|
||||
// If the performing user is and admin we don't care, they can access the group later regardless
|
||||
if (performingUser.IsAdmin() is false && userIds.Contains(performingUser.Id) is false)
|
||||
{
|
||||
userIds.Add(performingUser.Id);
|
||||
}
|
||||
|
||||
return userIds;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,9 @@ 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.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
using UserProfile = Umbraco.Cms.Core.Models.Membership.UserProfile;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -25,6 +27,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
private readonly ILogger<UserService> _logger;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly IUserGroupRepository _userGroupRepository;
|
||||
private readonly IUserGroupAuthorizationService _userGroupAuthorizationService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public UserService(
|
||||
@@ -34,12 +37,14 @@ internal class UserService : RepositoryService, IUserService
|
||||
IRuntimeState runtimeState,
|
||||
IUserRepository userRepository,
|
||||
IUserGroupRepository userGroupRepository,
|
||||
IOptions<GlobalSettings> globalSettings)
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IUserGroupAuthorizationService userGroupAuthorizationService)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
{
|
||||
_runtimeState = runtimeState;
|
||||
_userRepository = userRepository;
|
||||
_userGroupRepository = userGroupRepository;
|
||||
_userGroupAuthorizationService = userGroupAuthorizationService;
|
||||
_globalSettings = globalSettings.Value;
|
||||
_logger = loggerFactory.CreateLogger<UserService>();
|
||||
}
|
||||
@@ -884,6 +889,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
/// </summary>
|
||||
/// <param name="ids">Optional Ids of UserGroups to retrieve</param>
|
||||
/// <returns>An enumerable list of <see cref="IUserGroup" /></returns>
|
||||
[Obsolete("Use IUserGroupService.GetAsync instead, scheduled for removal in V15.")]
|
||||
public IEnumerable<IUserGroup> GetAllUserGroups(params int[] ids)
|
||||
{
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
@@ -892,6 +898,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use IUserGroupService.GetAsync instead, scheduled for removal in V15.")]
|
||||
public IEnumerable<IUserGroup> GetUserGroupsByAlias(params string[] aliases)
|
||||
{
|
||||
if (aliases.Length == 0)
|
||||
@@ -914,6 +921,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
/// <returns>
|
||||
/// <see cref="IUserGroup" />
|
||||
/// </returns>
|
||||
[Obsolete("Use IUserGroupService.GetAsync instead, scheduled for removal in V15.")]
|
||||
public IUserGroup? GetUserGroupByAlias(string alias)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(alias))
|
||||
@@ -936,6 +944,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
/// <returns>
|
||||
/// <see cref="IUserGroup" />
|
||||
/// </returns>
|
||||
[Obsolete("Use IUserGroupService.GetAsync instead, scheduled for removal in V15.")]
|
||||
public IUserGroup? GetUserGroupById(int id)
|
||||
{
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
@@ -958,6 +967,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
/// <c>False</c>
|
||||
/// to not raise events
|
||||
/// </param>
|
||||
[Obsolete("Use IUserGroupService.CreateAsync and IUserGroupService.UpdateAsync instead, scheduled for removal in V15.")]
|
||||
public void Save(IUserGroup userGroup, int[]? userIds = null)
|
||||
{
|
||||
EventMessages evtMsgs = EventMessagesFactory.Get();
|
||||
@@ -965,7 +975,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
||||
{
|
||||
// we need to figure out which users have been added / removed, for audit purposes
|
||||
var empty = new IUser[0];
|
||||
IUser[] empty = Array.Empty<IUser>();
|
||||
IUser[] addedUsers = empty;
|
||||
IUser[] removedUsers = empty;
|
||||
|
||||
@@ -1018,6 +1028,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
/// Deletes a UserGroup
|
||||
/// </summary>
|
||||
/// <param name="userGroup">UserGroup to delete</param>
|
||||
[Obsolete("Use IUserGroupService.DeleteAsync instead, scheduled for removal in V15.")]
|
||||
public void DeleteUserGroup(IUserGroup userGroup)
|
||||
{
|
||||
EventMessages evtMsgs = EventMessagesFactory.Get();
|
||||
|
||||
@@ -77,6 +77,13 @@
|
||||
<Right>lib/net7.0/Umbraco.Infrastructure.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Core.Security.BackOfficeUserStore.#ctor(Umbraco.Cms.Core.Scoping.ICoreScopeProvider,Umbraco.Cms.Core.Services.IUserService,Umbraco.Cms.Core.Services.IEntityService,Umbraco.Cms.Core.Services.IExternalLoginWithKeyService,Microsoft.Extensions.Options.IOptions{Umbraco.Cms.Core.Configuration.Models.GlobalSettings},Umbraco.Cms.Core.Mapping.IUmbracoMapper,Umbraco.Cms.Core.Security.BackOfficeErrorDescriber,Umbraco.Cms.Core.Cache.AppCaches)</Target>
|
||||
<Left>lib/net7.0/Umbraco.Infrastructure.dll</Left>
|
||||
<Right>lib/net7.0/Umbraco.Infrastructure.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Infrastructure.Install.PackageMigrationRunner.#ctor(Umbraco.Cms.Core.Logging.IProfilingLogger,Umbraco.Cms.Core.Scoping.ICoreScopeProvider,Umbraco.Cms.Core.Packaging.PendingPackageMigrations,Umbraco.Cms.Core.Packaging.PackageMigrationPlanCollection,Umbraco.Cms.Core.Migrations.IMigrationPlanExecutor,Umbraco.Cms.Core.Services.IKeyValueService,Umbraco.Cms.Core.Events.IEventAggregator)</Target>
|
||||
|
||||
@@ -68,6 +68,7 @@ public class DatabaseSchemaCreator
|
||||
typeof(User2UserGroupDto),
|
||||
typeof(UserGroup2NodePermissionDto),
|
||||
typeof(UserGroup2AppDto),
|
||||
typeof(UserGroup2PermissionDto),
|
||||
typeof(UserStartNodeDto),
|
||||
typeof(ContentNuDto),
|
||||
typeof(DocumentVersionDto),
|
||||
|
||||
@@ -10,6 +10,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations;
|
||||
|
||||
|
||||
@@ -83,5 +83,6 @@ public class UmbracoPlan : MigrationPlan
|
||||
To<AddPropertyEditorUiAliasColumn>("{419827A0-4FCE-464B-A8F3-247C6092AF55}");
|
||||
To<MigrateDataTypeConfigurations>("{5F15A1CC-353D-4889-8C7E-F303B4766196}");
|
||||
To<AddGuidsToUserGroups>("{69E12556-D9B3-493A-8E8A-65EC89FB658D}");
|
||||
To<AddUserGroup2PermisionTable>("{F2B16CD4-F181-4BEE-81C9-11CF384E6025}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using NPoco;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0;
|
||||
|
||||
public class AddUserGroup2PermisionTable : MigrationBase
|
||||
{
|
||||
public AddUserGroup2PermisionTable(IMigrationContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Migrate()
|
||||
{
|
||||
if (TableExists(Constants.DatabaseSchema.Tables.UserGroup2Permission))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Create.Table<UserGroup2PermissionDto>().Do();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
[TableName(Constants.DatabaseSchema.Tables.UserGroup2Permission)]
|
||||
[ExplicitColumns]
|
||||
public class UserGroup2PermissionDto
|
||||
{
|
||||
[PrimaryKeyColumn(Name = "PK_userGroup2Permission", AutoIncrement = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("userGroupId")]
|
||||
[Index(IndexTypes.NonClustered, IncludeColumns = "permission")]
|
||||
[ForeignKey(typeof(UserGroupDto))]
|
||||
public int UserGroupId { get; set; }
|
||||
|
||||
[Column("permission")]
|
||||
[SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
|
||||
public required string Permission { get; set; }
|
||||
}
|
||||
@@ -14,6 +14,7 @@ public class UserGroupDto
|
||||
{
|
||||
UserGroup2AppDtos = new List<UserGroup2AppDto>();
|
||||
UserGroup2LanguageDtos = new List<UserGroup2LanguageDto>();
|
||||
UserGroup2PermissionDtos = new List<UserGroup2PermissionDto>();
|
||||
}
|
||||
|
||||
[Column("id")]
|
||||
@@ -77,6 +78,10 @@ public class UserGroupDto
|
||||
[Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")]
|
||||
public List<UserGroup2LanguageDto> UserGroup2LanguageDtos { get; set; }
|
||||
|
||||
[ResultColumn]
|
||||
[Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")]
|
||||
public List<UserGroup2PermissionDto> UserGroup2PermissionDtos { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is only relevant when this column is included in the results (i.e. GetUserGroupsWithUserCounts)
|
||||
/// </summary>
|
||||
|
||||
@@ -27,6 +27,7 @@ internal static class UserGroupFactory
|
||||
userGroup.UpdateDate = dto.UpdateDate;
|
||||
userGroup.StartContentId = dto.StartContentId;
|
||||
userGroup.StartMediaId = dto.StartMediaId;
|
||||
userGroup.PermissionNames = dto.UserGroup2PermissionDtos.Select(x => x.Permission).ToHashSet();
|
||||
userGroup.HasAccessToAllLanguages = dto.HasAccessToAllLanguages;
|
||||
if (dto.UserGroup2AppDtos != null)
|
||||
{
|
||||
@@ -80,7 +81,7 @@ internal static class UserGroupFactory
|
||||
|
||||
if (entity.HasIdentity)
|
||||
{
|
||||
dto.Id = short.Parse(entity.Id.ToString());
|
||||
dto.Id = entity.Id;
|
||||
}
|
||||
|
||||
return dto;
|
||||
|
||||
@@ -289,6 +289,27 @@ internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended
|
||||
return Database.ExecuteScalar<int>(sql) > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Exists(Guid key, Guid objectType)
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.SelectCount()
|
||||
.From<NodeDto>()
|
||||
.Where<NodeDto>(x => x.UniqueId == key && x.NodeObjectType == objectType);
|
||||
|
||||
return Database.ExecuteScalar<int>(sql) > 0;
|
||||
}
|
||||
|
||||
public bool Exists(int id, Guid objectType)
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.SelectCount()
|
||||
.From<NodeDto>()
|
||||
.Where<NodeDto>(x => x.NodeId == id && x.NodeObjectType == objectType);
|
||||
|
||||
return Database.ExecuteScalar<int>(sql) > 0;
|
||||
}
|
||||
|
||||
public bool Exists(int id)
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql().SelectCount().From<NodeDto>().Where<NodeDto>(x => x.NodeId == id);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -25,7 +26,12 @@ public class UserGroupRepository : EntityRepositoryBase<int, IUserGroup>, IUserG
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly UserGroupWithUsersRepository _userGroupWithUsersRepository;
|
||||
|
||||
public UserGroupRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger<UserGroupRepository> logger, ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper)
|
||||
public UserGroupRepository(
|
||||
IScopeAccessor scopeAccessor,
|
||||
AppCaches appCaches,
|
||||
ILogger<UserGroupRepository> logger,
|
||||
ILoggerFactory loggerFactory,
|
||||
IShortStringHelper shortStringHelper)
|
||||
: base(scopeAccessor, appCaches, logger)
|
||||
{
|
||||
_shortStringHelper = shortStringHelper;
|
||||
@@ -299,6 +305,7 @@ public class UserGroupRepository : EntityRepositoryBase<int, IUserGroup>, IUserG
|
||||
}
|
||||
|
||||
dto.UserGroup2LanguageDtos = GetUserGroupLanguages(id);
|
||||
dto.UserGroup2PermissionDtos = GetUserGroupPermissions(id);
|
||||
|
||||
IUserGroup userGroup = UserGroupFactory.BuildEntity(_shortStringHelper, dto);
|
||||
return userGroup;
|
||||
@@ -322,13 +329,7 @@ public class UserGroupRepository : EntityRepositoryBase<int, IUserGroup>, IUserG
|
||||
|
||||
List<UserGroupDto> dtos = Database.FetchOneToMany<UserGroupDto>(x => x.UserGroup2AppDtos, sql);
|
||||
|
||||
IDictionary<int, List<UserGroup2LanguageDto>> dic = GetAllUserGroupLanguageGrouped();
|
||||
|
||||
foreach (UserGroupDto dto in dtos)
|
||||
{
|
||||
dic.TryGetValue(dto.Id, out var userGroup2LanguageDtos);
|
||||
dto.UserGroup2LanguageDtos = userGroup2LanguageDtos ?? new();
|
||||
}
|
||||
AssignUserGroupOneToManyTables(ref dtos);
|
||||
|
||||
return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x));
|
||||
}
|
||||
@@ -342,10 +343,28 @@ public class UserGroupRepository : EntityRepositoryBase<int, IUserGroup>, IUserG
|
||||
AppendGroupBy(sql);
|
||||
sql.OrderBy<UserGroupDto>(x => x.Id); // required for references
|
||||
|
||||
List<UserGroupDto>? dtos = Database.FetchOneToMany<UserGroupDto>(x => x.UserGroup2AppDtos, sql);
|
||||
List<UserGroupDto> dtos = Database.FetchOneToMany<UserGroupDto>(x => x.UserGroup2AppDtos, sql);
|
||||
|
||||
AssignUserGroupOneToManyTables(ref dtos);
|
||||
|
||||
return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x));
|
||||
}
|
||||
|
||||
private void AssignUserGroupOneToManyTables(ref List<UserGroupDto> userGroupDtos)
|
||||
{
|
||||
IDictionary<int, List<UserGroup2LanguageDto>> userGroups2Languages = GetAllUserGroupLanguageGrouped();
|
||||
IDictionary<int, List<UserGroup2PermissionDto>> userGroups2Permissions = GetAllUserGroupPermissionsGrouped();
|
||||
|
||||
foreach (UserGroupDto dto in userGroupDtos)
|
||||
{
|
||||
userGroups2Languages.TryGetValue(dto.Id, out List<UserGroup2LanguageDto>? userGroup2LanguageDtos);
|
||||
dto.UserGroup2LanguageDtos = userGroup2LanguageDtos ?? new List<UserGroup2LanguageDto>();
|
||||
|
||||
userGroups2Permissions.TryGetValue(dto.Id, out List<UserGroup2PermissionDto>? userGroup2PermissionDtos);
|
||||
dto.UserGroup2PermissionDtos = userGroup2PermissionDtos ?? new List<UserGroup2PermissionDto>();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Overrides of EntityRepositoryBase<int,IUserGroup>
|
||||
@@ -417,6 +436,7 @@ public class UserGroupRepository : EntityRepositoryBase<int, IUserGroup>, IUserG
|
||||
"DELETE FROM umbracoUserGroup2App WHERE userGroupId = @id",
|
||||
"DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @id",
|
||||
"DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @id",
|
||||
"DELETE FROM umbracoUserGroup2Permission WHERE userGroupId = @id",
|
||||
"DELETE FROM umbracoUserGroup WHERE id = @id",
|
||||
};
|
||||
return list;
|
||||
@@ -433,6 +453,7 @@ public class UserGroupRepository : EntityRepositoryBase<int, IUserGroup>, IUserG
|
||||
|
||||
PersistAllowedSections(entity);
|
||||
PersistAllowedLanguages(entity);
|
||||
PersistPermissions(entity);
|
||||
|
||||
entity.ResetDirtyProperties();
|
||||
}
|
||||
@@ -446,7 +467,8 @@ public class UserGroupRepository : EntityRepositoryBase<int, IUserGroup>, IUserG
|
||||
Database.Update(userGroupDto);
|
||||
|
||||
PersistAllowedSections(entity);
|
||||
PersistAllowedLanguages(entity);
|
||||
PersistAllowedLanguages(entity);
|
||||
PersistPermissions(entity);
|
||||
|
||||
entity.ResetDirtyProperties();
|
||||
}
|
||||
@@ -466,43 +488,74 @@ public class UserGroupRepository : EntityRepositoryBase<int, IUserGroup>, IUserG
|
||||
}
|
||||
}
|
||||
|
||||
private void PersistAllowedLanguages(IUserGroup entity)
|
||||
private void PersistAllowedLanguages(IUserGroup entity)
|
||||
{
|
||||
var userGroup = entity;
|
||||
|
||||
// First delete all
|
||||
Database.Delete<UserGroup2LanguageDto>("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id });
|
||||
|
||||
// Then re-add any associated with the group
|
||||
foreach (var language in userGroup.AllowedLanguages)
|
||||
{
|
||||
var userGroup = entity;
|
||||
|
||||
// First delete all
|
||||
Database.Delete<UserGroup2LanguageDto>("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id });
|
||||
|
||||
// Then re-add any associated with the group
|
||||
foreach (var language in userGroup.AllowedLanguages)
|
||||
var dto = new UserGroup2LanguageDto
|
||||
{
|
||||
var dto = new UserGroup2LanguageDto
|
||||
{
|
||||
UserGroupId = userGroup.Id,
|
||||
LanguageId = language,
|
||||
};
|
||||
UserGroupId = userGroup.Id,
|
||||
LanguageId = language,
|
||||
};
|
||||
|
||||
Database.Insert(dto);
|
||||
}
|
||||
Database.Insert(dto);
|
||||
}
|
||||
}
|
||||
|
||||
private List<UserGroup2LanguageDto> GetUserGroupLanguages(int userGroupId)
|
||||
private void PersistPermissions(IUserGroup userGroup)
|
||||
{
|
||||
Database.Delete<UserGroup2PermissionDto>("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id });
|
||||
|
||||
foreach (var permission in userGroup.PermissionNames)
|
||||
{
|
||||
Sql<ISqlContext> query = Sql()
|
||||
.Select<UserGroup2LanguageDto>()
|
||||
.From<UserGroup2LanguageDto>()
|
||||
.Where<UserGroup2LanguageDto>(x => x.UserGroupId == userGroupId);
|
||||
return Database.Fetch<UserGroup2LanguageDto>(query);
|
||||
var permissionDto = new UserGroup2PermissionDto { UserGroupId = userGroup.Id, Permission = permission, };
|
||||
Database.Insert(permissionDto);
|
||||
}
|
||||
}
|
||||
|
||||
private IDictionary<int, List<UserGroup2LanguageDto>> GetAllUserGroupLanguageGrouped()
|
||||
{
|
||||
Sql<ISqlContext> query = Sql()
|
||||
.Select<UserGroup2LanguageDto>()
|
||||
.From<UserGroup2LanguageDto>();
|
||||
List<UserGroup2LanguageDto> userGroupLanguages = Database.Fetch<UserGroup2LanguageDto>(query);
|
||||
return userGroupLanguages.GroupBy(x => x.UserGroupId).ToDictionary(x => x.Key, x => x.ToList());
|
||||
}
|
||||
private List<UserGroup2LanguageDto> GetUserGroupLanguages(int userGroupId)
|
||||
{
|
||||
Sql<ISqlContext> query = Sql()
|
||||
.Select<UserGroup2LanguageDto>()
|
||||
.From<UserGroup2LanguageDto>()
|
||||
.Where<UserGroup2LanguageDto>(x => x.UserGroupId == userGroupId);
|
||||
return Database.Fetch<UserGroup2LanguageDto>(query);
|
||||
}
|
||||
|
||||
private IDictionary<int, List<UserGroup2LanguageDto>> GetAllUserGroupLanguageGrouped()
|
||||
{
|
||||
Sql<ISqlContext> query = Sql()
|
||||
.Select<UserGroup2LanguageDto>()
|
||||
.From<UserGroup2LanguageDto>();
|
||||
List<UserGroup2LanguageDto> userGroupLanguages = Database.Fetch<UserGroup2LanguageDto>(query);
|
||||
return userGroupLanguages.GroupBy(x => x.UserGroupId).ToDictionary(x => x.Key, x => x.ToList());
|
||||
}
|
||||
|
||||
private List<UserGroup2PermissionDto> GetUserGroupPermissions(int userGroupId)
|
||||
{
|
||||
Sql<ISqlContext> query = Sql()
|
||||
.Select<UserGroup2PermissionDto>()
|
||||
.From<UserGroup2PermissionDto>()
|
||||
.Where<UserGroup2PermissionDto>(x => x.UserGroupId == userGroupId);
|
||||
|
||||
return Database.Fetch<UserGroup2PermissionDto>(query);
|
||||
}
|
||||
|
||||
private Dictionary<int, List<UserGroup2PermissionDto>> GetAllUserGroupPermissionsGrouped()
|
||||
{
|
||||
Sql<ISqlContext> query = Sql()
|
||||
.Select<UserGroup2PermissionDto>()
|
||||
.From<UserGroup2PermissionDto>();
|
||||
|
||||
List<UserGroup2PermissionDto> userGroupPermissions = Database.Fetch<UserGroup2PermissionDto>(query);
|
||||
return userGroupPermissions.GroupBy(x => x.UserGroupId).ToDictionary(x => x.Key, x => x.ToList());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public class BackOfficeUserStore : UmbracoUserStore<BackOfficeIdentityUser, Iden
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
private readonly ICoreScopeProvider _scopeProvider;
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +44,8 @@ public class BackOfficeUserStore : UmbracoUserStore<BackOfficeIdentityUser, Iden
|
||||
IUmbracoMapper mapper,
|
||||
BackOfficeErrorDescriber describer,
|
||||
AppCaches appCaches,
|
||||
ITwoFactorLoginService twoFactorLoginService)
|
||||
ITwoFactorLoginService twoFactorLoginService,
|
||||
IUserGroupService userGroupService)
|
||||
: base(describer)
|
||||
{
|
||||
_scopeProvider = scopeProvider;
|
||||
@@ -54,30 +56,33 @@ public class BackOfficeUserStore : UmbracoUserStore<BackOfficeIdentityUser, Iden
|
||||
_mapper = mapper;
|
||||
_appCaches = appCaches;
|
||||
_twoFactorLoginService = twoFactorLoginService;
|
||||
_userGroupService = userGroupService;
|
||||
_userService = userService;
|
||||
_externalLoginService = externalLoginService;
|
||||
}
|
||||
|
||||
[Obsolete("Use non obsolete ctor")]
|
||||
[Obsolete("Use constructor that takes IUserGroupService, scheduled for removal in V15.")]
|
||||
public BackOfficeUserStore(
|
||||
ICoreScopeProvider scopeProvider,
|
||||
IUserService userService,
|
||||
IEntityService entityService,
|
||||
IExternalLoginWithKeyService externalLoginService,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IOptionsSnapshot<GlobalSettings> globalSettings,
|
||||
IUmbracoMapper mapper,
|
||||
BackOfficeErrorDescriber describer,
|
||||
AppCaches appCaches)
|
||||
AppCaches appCaches,
|
||||
ITwoFactorLoginService twoFactorLoginService)
|
||||
: this(
|
||||
scopeProvider,
|
||||
userService,
|
||||
entityService,
|
||||
externalLoginService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IOptionsSnapshot<GlobalSettings>>(),
|
||||
globalSettings,
|
||||
mapper,
|
||||
describer,
|
||||
appCaches,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ITwoFactorLoginService>())
|
||||
twoFactorLoginService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserGroupService>())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -390,7 +395,7 @@ public class BackOfficeUserStore : UmbracoUserStore<BackOfficeIdentityUser, Iden
|
||||
throw new ArgumentNullException(nameof(normalizedRoleName));
|
||||
}
|
||||
|
||||
IUserGroup? userGroup = _userService.GetUserGroupByAlias(normalizedRoleName);
|
||||
IUserGroup? userGroup = _userGroupService.GetAsync(normalizedRoleName).GetAwaiter().GetResult();
|
||||
|
||||
IEnumerable<IUser> users = _userService.GetAllInGroup(userGroup?.Id);
|
||||
IList<BackOfficeIdentityUser> backOfficeIdentityUsers =
|
||||
@@ -495,7 +500,7 @@ public class BackOfficeUserStore : UmbracoUserStore<BackOfficeIdentityUser, Iden
|
||||
string normalizedRoleName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IUserGroup? group = _userService.GetUserGroupByAlias(normalizedRoleName);
|
||||
IUserGroup? group = _userGroupService.GetAsync(normalizedRoleName).GetAwaiter().GetResult();
|
||||
if (group?.Name is null)
|
||||
{
|
||||
return Task.FromResult<IdentityRole<string>?>(null);
|
||||
@@ -670,7 +675,7 @@ public class BackOfficeUserStore : UmbracoUserStore<BackOfficeIdentityUser, Iden
|
||||
user.ClearGroups();
|
||||
|
||||
// go lookup all these groups
|
||||
IReadOnlyUserGroup[] groups = _userService.GetUserGroupsByAlias(identityUserRoles)
|
||||
IReadOnlyUserGroup[] groups = _userGroupService.GetAsync(identityUserRoles).GetAwaiter().GetResult()
|
||||
.Select(x => x.ToReadOnlyGroup()).ToArray();
|
||||
|
||||
// use all of the ones assigned and add them
|
||||
|
||||
@@ -31,12 +31,36 @@ public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, I
|
||||
{
|
||||
JsonTypeInfo result = base.GetTypeInfo(type, options);
|
||||
|
||||
if (!type.IsInterface)
|
||||
if (type.IsInterface)
|
||||
{
|
||||
return GetTypeInfoForInterface(result, type, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetTypeInfoForClass(result, type, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private JsonTypeInfo GetTypeInfoForClass(JsonTypeInfo result, Type type, JsonSerializerOptions options)
|
||||
{
|
||||
if (result.Kind != JsonTypeInfoKind.Object || !type.GetInterfaces().Any())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
Type[] subTypes = FindSubTypes(type).ToArray();
|
||||
JsonPolymorphismOptions jsonPolymorphismOptions = result.PolymorphismOptions ?? new JsonPolymorphismOptions();
|
||||
|
||||
jsonPolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(type, type.Name));
|
||||
|
||||
result.PolymorphismOptions = jsonPolymorphismOptions;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonTypeInfo GetTypeInfoForInterface(JsonTypeInfo result, Type type, JsonSerializerOptions options)
|
||||
{
|
||||
IEnumerable<Type> subTypes = FindSubTypes(type);
|
||||
|
||||
if (!subTypes.Any())
|
||||
{
|
||||
@@ -56,6 +80,7 @@ public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, I
|
||||
|
||||
result.PolymorphismOptions = jsonPolymorphismOptions;
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Telemetry.Interfaces;
|
||||
@@ -7,14 +9,26 @@ namespace Umbraco.Cms.Infrastructure.Telemetry.Providers;
|
||||
|
||||
public class UserTelemetryProvider : IDetailedTelemetryProvider
|
||||
{
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public UserTelemetryProvider(IUserService userService) => _userService = userService;
|
||||
[Obsolete("Use constructor that takes IUserGroupService, scheduled for removal in V15")]
|
||||
public UserTelemetryProvider(IUserService userService)
|
||||
: this(userService, StaticServiceProvider.Instance.GetRequiredService<IUserGroupService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Use constructor that only takes IUserGroupService, scheduled for removal in V15")]
|
||||
public UserTelemetryProvider(IUserService userService, IUserGroupService userGroupService)
|
||||
{
|
||||
_userService = userService;
|
||||
_userGroupService = userGroupService;
|
||||
}
|
||||
|
||||
public IEnumerable<UsageInformation> GetInformation()
|
||||
{
|
||||
_userService.GetAll(1, 1, out var total);
|
||||
var userGroups = _userService.GetAllUserGroups().Count();
|
||||
var userGroups = _userGroupService.GetAllAsync(0, int.MaxValue).GetAwaiter().GetResult().Items.Count();
|
||||
|
||||
yield return new UsageInformation(Constants.Telemetry.UserCount, total);
|
||||
yield return new UsageInformation(Constants.Telemetry.UserGroupCount, userGroups);
|
||||
|
||||
10
src/Umbraco.Web.BackOffice/CompatibilitySuppressions.xml
Normal file
10
src/Umbraco.Web.BackOffice/CompatibilitySuppressions.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Web.BackOffice.Controllers.ContentController.#ctor(Umbraco.Cms.Core.Dictionary.ICultureDictionary,Microsoft.Extensions.Logging.ILoggerFactory,Umbraco.Cms.Core.Strings.IShortStringHelper,Umbraco.Cms.Core.Events.IEventMessagesFactory,Umbraco.Cms.Core.Services.ILocalizedTextService,Umbraco.Cms.Core.PropertyEditors.PropertyEditorCollection,Umbraco.Cms.Core.Services.IContentService,Umbraco.Cms.Core.Services.IUserService,Umbraco.Cms.Core.Security.IBackOfficeSecurityAccessor,Umbraco.Cms.Core.Services.IContentTypeService,Umbraco.Cms.Core.Mapping.IUmbracoMapper,Umbraco.Cms.Core.Routing.IPublishedUrlProvider,Umbraco.Cms.Core.Services.IDomainService,Umbraco.Cms.Core.Services.IDataTypeService,Umbraco.Cms.Core.Services.ILocalizationService,Umbraco.Cms.Core.Services.IFileService,Umbraco.Cms.Core.Services.INotificationService,Umbraco.Cms.Core.Actions.ActionCollection,Umbraco.Cms.Infrastructure.Persistence.ISqlContext,Umbraco.Cms.Core.Serialization.IJsonSerializer,Umbraco.Cms.Core.Scoping.ICoreScopeProvider,Microsoft.AspNetCore.Authorization.IAuthorizationService,Umbraco.Cms.Core.Services.IContentVersionService)</Target>
|
||||
<Left>lib/net7.0/Umbraco.Web.BackOffice.dll</Left>
|
||||
<Right>lib/net7.0/Umbraco.Web.BackOffice.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
@@ -58,6 +58,7 @@ public class ContentController : ContentControllerBase
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly ICultureImpactFactory _cultureImpactFactory;
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly ILogger<ContentController> _logger;
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IPublishedUrlProvider _publishedUrlProvider;
|
||||
@@ -91,7 +92,8 @@ public class ContentController : ContentControllerBase
|
||||
ICoreScopeProvider scopeProvider,
|
||||
IAuthorizationService authorizationService,
|
||||
IContentVersionService contentVersionService,
|
||||
ICultureImpactFactory cultureImpactFactory)
|
||||
ICultureImpactFactory cultureImpactFactory,
|
||||
IUserGroupService userGroupService)
|
||||
: base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, serializer)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
@@ -112,13 +114,14 @@ public class ContentController : ContentControllerBase
|
||||
_authorizationService = authorizationService;
|
||||
_contentVersionService = contentVersionService;
|
||||
_cultureImpactFactory = cultureImpactFactory;
|
||||
_userGroupService = userGroupService;
|
||||
_logger = loggerFactory.CreateLogger<ContentController>();
|
||||
_scopeProvider = scopeProvider;
|
||||
_allLangs = new Lazy<IDictionary<string, ILanguage>>(() =>
|
||||
_localizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase));
|
||||
_localizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
[Obsolete("Use constructor that accepts ICultureImpactService as a parameter, scheduled for removal in V12")]
|
||||
[Obsolete("User constructor that takes a IUserGroupService, scheduled for removal in V15.")]
|
||||
public ContentController(
|
||||
ICultureDictionary cultureDictionary,
|
||||
ILoggerFactory loggerFactory,
|
||||
@@ -142,7 +145,8 @@ public class ContentController : ContentControllerBase
|
||||
IJsonSerializer serializer,
|
||||
ICoreScopeProvider scopeProvider,
|
||||
IAuthorizationService authorizationService,
|
||||
IContentVersionService contentVersionService)
|
||||
IContentVersionService contentVersionService,
|
||||
ICultureImpactFactory cultureImpactFactory)
|
||||
: this(
|
||||
cultureDictionary,
|
||||
loggerFactory,
|
||||
@@ -167,9 +171,11 @@ public class ContentController : ContentControllerBase
|
||||
scopeProvider,
|
||||
authorizationService,
|
||||
contentVersionService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICultureImpactFactory>())
|
||||
{
|
||||
}
|
||||
cultureImpactFactory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserGroupService>()
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public object? Domains { get; private set; }
|
||||
|
||||
@@ -224,7 +230,7 @@ public class ContentController : ContentControllerBase
|
||||
var contentPermissions = _contentService.GetPermissions(content)
|
||||
.ToDictionary(x => x.UserGroupId, x => x);
|
||||
|
||||
IUserGroup[] allUserGroups = _userService.GetAllUserGroups().ToArray();
|
||||
IUserGroup[] allUserGroups = _userGroupService.GetAllAsync(0, int.MaxValue).GetAwaiter().GetResult().Items.ToArray();
|
||||
|
||||
//loop through each user group
|
||||
foreach (IUserGroup userGroup in allUserGroups)
|
||||
@@ -277,7 +283,7 @@ public class ContentController : ContentControllerBase
|
||||
|
||||
// TODO: Should non-admins be able to see detailed permissions?
|
||||
|
||||
IEnumerable<IUserGroup> allUserGroups = _userService.GetAllUserGroups();
|
||||
IEnumerable<IUserGroup> allUserGroups = _userGroupService.GetAllAsync(0, int.MaxValue).GetAwaiter().GetResult().Items;
|
||||
|
||||
return GetDetailedPermissions(content, allUserGroups);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Web.BackOffice.Controllers;
|
||||
|
||||
[Obsolete("Use IUserGroupAuthorizationService instead, should be removed alongside UserGroup controller in V13.")]
|
||||
internal class UserGroupEditorAuthorizationHelper
|
||||
{
|
||||
private readonly AppCaches _appCaches;
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers;
|
||||
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
|
||||
[Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)]
|
||||
[PrefixlessBodyModelValidator]
|
||||
[Obsolete("Use the new user group controllers instead.")]
|
||||
public class UserGroupsController : BackOfficeNotificationsController
|
||||
{
|
||||
private readonly AppCaches _appCaches;
|
||||
|
||||
@@ -20,13 +20,14 @@ internal sealed class UserGroupValidateAttribute : TypeFilterAttribute
|
||||
private class UserGroupValidateFilter : IActionFilter
|
||||
{
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
|
||||
public UserGroupValidateFilter(
|
||||
IUserService userService,
|
||||
IUserGroupService userGroupService,
|
||||
IShortStringHelper shortStringHelper)
|
||||
{
|
||||
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
|
||||
ArgumentNullException.ThrowIfNull(userGroupService);
|
||||
_userGroupService = userGroupService;
|
||||
_shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper));
|
||||
}
|
||||
|
||||
@@ -45,7 +46,7 @@ internal sealed class UserGroupValidateAttribute : TypeFilterAttribute
|
||||
switch (userGroupSave?.Action)
|
||||
{
|
||||
case ContentSaveAction.Save:
|
||||
persisted = _userService.GetUserGroupById(Convert.ToInt32(userGroupSave.Id));
|
||||
persisted = _userGroupService.GetAsync(Convert.ToInt32(userGroupSave.Id)).GetAwaiter().GetResult();
|
||||
if (persisted == null)
|
||||
{
|
||||
var message = $"User group with id: {userGroupSave.Id} was not found";
|
||||
@@ -73,7 +74,7 @@ internal sealed class UserGroupValidateAttribute : TypeFilterAttribute
|
||||
//now assign the persisted entity to the model so we can use it in the action
|
||||
userGroupSave.PersistedUserGroup = persisted;
|
||||
|
||||
IUserGroup? existing = _userService.GetUserGroupByAlias(userGroupSave.Alias);
|
||||
IUserGroup? existing = _userGroupService.GetAsync(userGroupSave.Alias).GetAwaiter().GetResult();
|
||||
if (existing != null && existing.Id != userGroupSave.PersistedUserGroup.Id)
|
||||
{
|
||||
context.ModelState.AddModelError("Alias", "A user group with this alias already exists");
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
public class UserGroupServiceValidationTests : UmbracoIntegrationTest
|
||||
{
|
||||
private IUserGroupService UserGroupService => GetRequiredService<IUserGroupService>();
|
||||
|
||||
private IShortStringHelper ShortStringHelper => GetRequiredService<IShortStringHelper>();
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_create_user_group_with_name_equals_null()
|
||||
{
|
||||
var userGroup = new UserGroup(ShortStringHelper)
|
||||
{
|
||||
Name = null
|
||||
};
|
||||
|
||||
var result = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserId);
|
||||
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.MissingName, result.Status);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_create_user_group_with_name_longer_than_max_length()
|
||||
{
|
||||
var userGroup = new UserGroup(ShortStringHelper)
|
||||
{
|
||||
Name = "Sed porttitor lectus nibh. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Vivamus suscipit tortor eget felis porttitor volutpat. Quisque velit nisi, pretium ut lacinia in, elementum id enim."
|
||||
};
|
||||
|
||||
var result = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserId);
|
||||
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.NameTooLong, result.Status);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_create_user_group_with_alias_longer_than_max_length()
|
||||
{
|
||||
var userGroup = new UserGroup(ShortStringHelper)
|
||||
{
|
||||
Name = "Some Name",
|
||||
Alias = "Sed porttitor lectus nibh. Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Vivamus suscipit tortor eget felis porttitor volutpat. Quisque velit nisi, pretium ut lacinia in, elementum id enim. Vivamus suscipit tortor eget felis porttitor volutpat. Quisque velit nisi, pretium ut lacinia in, elementum id enim."
|
||||
};
|
||||
|
||||
var result = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserId);
|
||||
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.AliasTooLong, result.Status);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_update_non_existing_user_group()
|
||||
{
|
||||
var userGroup = new UserGroup(ShortStringHelper)
|
||||
{
|
||||
Name = "Some Name",
|
||||
Alias = "someAlias"
|
||||
};
|
||||
|
||||
var result = await UserGroupService.UpdateAsync(userGroup, Constants.Security.SuperUserId);
|
||||
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.NotFound, result.Status);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_create_existing_user_group()
|
||||
{
|
||||
var userGroup = new UserGroup(ShortStringHelper)
|
||||
{
|
||||
Name = "Some Name",
|
||||
Alias = "someAlias"
|
||||
};
|
||||
|
||||
var result = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserId);
|
||||
|
||||
Assert.IsTrue(result.Success);
|
||||
|
||||
result = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserId);
|
||||
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.AlreadyExists, result.Status);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_create_user_group_with_duplicate_alias()
|
||||
{
|
||||
var alias = "duplicateAlias";
|
||||
|
||||
var existingUserGroup = new UserGroup(ShortStringHelper)
|
||||
{
|
||||
Name = "I already exist",
|
||||
Alias = alias
|
||||
};
|
||||
var setupResult = await UserGroupService.CreateAsync(existingUserGroup, Constants.Security.SuperUserId);
|
||||
Assert.IsTrue(setupResult.Success);
|
||||
|
||||
var newUserGroup = new UserGroup(ShortStringHelper)
|
||||
{
|
||||
Name = "I have a duplicate alias",
|
||||
Alias = alias,
|
||||
};
|
||||
var result = await UserGroupService.CreateAsync(newUserGroup, Constants.Security.SuperUserId);
|
||||
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.DuplicateAlias, result.Status);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_update_user_group_with_duplicate_alias()
|
||||
{
|
||||
var alias = "duplicateAlias";
|
||||
|
||||
var existingUserGroup = new UserGroup(ShortStringHelper)
|
||||
{
|
||||
Name = "I already exist",
|
||||
Alias = alias
|
||||
};
|
||||
var setupResult = await UserGroupService.CreateAsync(existingUserGroup, Constants.Security.SuperUserId);
|
||||
Assert.IsTrue(setupResult.Success);
|
||||
|
||||
IUserGroup userGroupToUpdate = new UserGroup(ShortStringHelper)
|
||||
{
|
||||
Name = "I don't have a duplicate alias",
|
||||
Alias = "somAlias",
|
||||
};
|
||||
var creationResult = await UserGroupService.CreateAsync(userGroupToUpdate, Constants.Security.SuperUserId);
|
||||
Assert.IsTrue(creationResult.Success);
|
||||
|
||||
|
||||
userGroupToUpdate = creationResult.Result;
|
||||
userGroupToUpdate.Name = "Now I have a duplicate alias";
|
||||
userGroupToUpdate.Alias = alias;
|
||||
|
||||
var updateResult = await UserGroupService.UpdateAsync(userGroupToUpdate, Constants.Security.SuperUserId);
|
||||
Assert.IsFalse(updateResult.Success);
|
||||
Assert.AreEqual(UserGroupOperationStatus.DuplicateAlias, updateResult.Status);
|
||||
}
|
||||
}
|
||||
@@ -266,7 +266,8 @@ public class ContentControllerTests
|
||||
Mock.Of<ICoreScopeProvider>(),
|
||||
Mock.Of<IAuthorizationService>(),
|
||||
Mock.Of<IContentVersionService>(),
|
||||
Mock.Of<ICultureImpactFactory>());
|
||||
Mock.Of<ICultureImpactFactory>(),
|
||||
Mock.Of<IUserGroupService>());
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user