Files
Umbraco-CMS/src/Umbraco.Cms.Api.Management/Factories/UserGroupViewModelFactory.cs
Mole 5182f46bdb 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>
2023-02-16 09:39:17 +01:00

227 lines
8.4 KiB
C#

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;
}
}