* 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>
227 lines
8.4 KiB
C#
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;
|
|
}
|
|
}
|