New backoffice: Add GUIDS to user groups (#13672)

* 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 UniqueId to AppendGroupBy

Otherwise MSSQL grenades

* Cleanup

* Update CompatibilitySuppressions

* Rename UniqueId to Key

* Cleanup

---------

Co-authored-by: Elitsa Marinovska <elm@umbraco.dk>
This commit is contained in:
Mole
2023-02-07 08:30:33 +01:00
committed by GitHub
parent fdd059fb2b
commit 6808a63e73
16 changed files with 301 additions and 43 deletions

View File

@@ -160,6 +160,8 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase<SqliteSyntaxProvider>
public override string ConvertDateToOrderableString => "{0}";
public override string RenameTable => "ALTER TABLE {0} RENAME TO {1}";
/// <inheritdoc />
public override string GetSpecialDbType(SpecialDbType dbType) => "TEXT COLLATE NOCASE";

View File

@@ -203,6 +203,20 @@
<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>
<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.String},System.Collections.Generic.IEnumerable{System.String})</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.PropertyEditors.ConfigurationEditor.FromConfigurationEditor(System.Collections.Generic.IDictionary{System.String,System.Object},System.Object)</Target>
@@ -329,6 +343,13 @@
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.Implement.DataTypeService.#ctor(Umbraco.Cms.Core.PropertyEditors.IDataValueEditorFactory,Umbraco.Cms.Core.Scoping.ICoreScopeProvider,Microsoft.Extensions.Logging.ILoggerFactory,Umbraco.Cms.Core.Events.IEventMessagesFactory,Umbraco.Cms.Core.Persistence.Repositories.IDataTypeRepository,Umbraco.Cms.Core.Persistence.Repositories.IDataTypeContainerRepository,Umbraco.Cms.Core.Persistence.Repositories.IAuditRepository,Umbraco.Cms.Core.Persistence.Repositories.IEntityRepository,Umbraco.Cms.Core.Persistence.Repositories.IContentTypeRepository,Umbraco.Cms.Core.IO.IIOHelper,Umbraco.Cms.Core.Services.ILocalizedTextService,Umbraco.Cms.Core.Services.ILocalizationService,Umbraco.Cms.Core.Strings.IShortStringHelper,Umbraco.Cms.Core.Serialization.IJsonSerializer)</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.Extensions.DictionaryItemExtensions.GetDefaultValue(Umbraco.Cms.Core.Models.IDictionaryItem)</Target>
@@ -343,13 +364,6 @@
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.Implement.DataTypeService.#ctor(Umbraco.Cms.Core.PropertyEditors.IDataValueEditorFactory,Umbraco.Cms.Core.Scoping.ICoreScopeProvider,Microsoft.Extensions.Logging.ILoggerFactory,Umbraco.Cms.Core.Events.IEventMessagesFactory,Umbraco.Cms.Core.Persistence.Repositories.IDataTypeRepository,Umbraco.Cms.Core.Persistence.Repositories.IDataTypeContainerRepository,Umbraco.Cms.Core.Persistence.Repositories.IAuditRepository,Umbraco.Cms.Core.Persistence.Repositories.IEntityRepository,Umbraco.Cms.Core.Persistence.Repositories.IContentTypeRepository,Umbraco.Cms.Core.IO.IIOHelper,Umbraco.Cms.Core.Services.ILocalizedTextService,Umbraco.Cms.Core.Services.ILocalizationService,Umbraco.Cms.Core.Strings.IShortStringHelper,Umbraco.Cms.Core.Serialization.IJsonSerializer)</Target>
<Left>lib/net7.0/Umbraco.Core.dll</Left>
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Deploy.IDataTypeConfigurationConnector.FromArtifact(Umbraco.Cms.Core.Models.IDataType,System.String,Umbraco.Cms.Core.Deploy.IContextCache)</Target>
@@ -574,4 +588,4 @@
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
</Suppressions>

View File

@@ -11,6 +11,8 @@ public interface IReadOnlyUserGroup
int Id { get; }
Guid Key => Guid.Empty;
int? StartContentId { get; }
int? StartMediaId { get; }

View File

@@ -4,6 +4,7 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
{
public ReadOnlyUserGroup(
int id,
Guid key,
string? name,
string? icon,
int? startContentId,
@@ -17,6 +18,7 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
Name = name ?? string.Empty;
Icon = icon;
Id = id;
Key = key;
Alias = alias ?? string.Empty;
AllowedLanguages = allowedLanguages.ToArray();
AllowedSections = allowedSections.ToArray();
@@ -28,22 +30,10 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
HasAccessToAllLanguages = hasAccessToAllLanguages;
}
[Obsolete("please use ctor that takes allowedActions & hasAccessToAllLanguages instead, scheduled for removal in v12")]
public ReadOnlyUserGroup(
int id,
string? name,
string? icon,
int? startContentId,
int? startMediaId,
string? alias,
IEnumerable<string> allowedSections,
IEnumerable<string>? permissions)
: this(id, name, icon, startContentId, startMediaId, alias, Enumerable.Empty<int>(), allowedSections, permissions, true)
{
}
public int Id { get; }
public Guid Key { get; }
public bool Equals(ReadOnlyUserGroup? other)
{
if (ReferenceEquals(null, other))

View File

@@ -14,7 +14,18 @@ public static class UserGroupExtensions
}
// otherwise create one
return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.AllowedLanguages, group.AllowedSections, group.Permissions, group.HasAccessToAllLanguages);
return new ReadOnlyUserGroup(
group.Id,
group.Key,
group.Name,
group.Icon,
group.StartContentId,
group.StartMediaId,
group.Alias,
group.AllowedLanguages,
group.AllowedSections,
group.Permissions,
group.HasAccessToAllLanguages);
}
public static bool IsSystemUserGroup(this IUserGroup group) =>

View File

@@ -1165,10 +1165,14 @@ internal class DatabaseDataCreator
private void CreateUserGroupData()
{
_database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false,
_database.Insert(
Constants.DatabaseSchema.Tables.UserGroup,
"id",
false,
new UserGroupDto
{
Id = 1,
Key = Guid.NewGuid(),
StartMediaId = -1,
StartContentId = -1,
Alias = Constants.Security.AdminGroupAlias,
@@ -1179,10 +1183,14 @@ internal class DatabaseDataCreator
Icon = "icon-medal",
HasAccessToAllLanguages = true,
});
_database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false,
_database.Insert(
Constants.DatabaseSchema.Tables.UserGroup,
"id",
false,
new UserGroupDto
{
Id = 2,
Key = Guid.NewGuid(),
StartMediaId = -1,
StartContentId = -1,
Alias = Constants.Security.WriterGroupAlias,
@@ -1193,10 +1201,14 @@ internal class DatabaseDataCreator
Icon = "icon-edit",
HasAccessToAllLanguages = true,
});
_database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false,
_database.Insert(
Constants.DatabaseSchema.Tables.UserGroup,
"id",
false,
new UserGroupDto
{
Id = 3,
Key = Guid.NewGuid(),
StartMediaId = -1,
StartContentId = -1,
Alias = Constants.Security.EditorGroupAlias,
@@ -1207,10 +1219,14 @@ internal class DatabaseDataCreator
Icon = "icon-tools",
HasAccessToAllLanguages = true,
});
_database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false,
_database.Insert(
Constants.DatabaseSchema.Tables.UserGroup,
"id",
false,
new UserGroupDto
{
Id = 4,
Key = Guid.NewGuid(),
StartMediaId = -1,
StartContentId = -1,
Alias = Constants.Security.TranslatorGroupAlias,
@@ -1221,10 +1237,14 @@ internal class DatabaseDataCreator
Icon = "icon-globe",
HasAccessToAllLanguages = true,
});
_database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false,
_database.Insert(
Constants.DatabaseSchema.Tables.UserGroup,
"id",
false,
new UserGroupDto
{
Id = 5,
Key = Guid.NewGuid(),
Alias = Constants.Security.SensitiveDataGroupAlias,
Name = "Sensitive data",
DefaultPermissions = string.Empty,

View File

@@ -24,7 +24,7 @@ internal class MigrationContext : IMigrationContext
public MigrationPlan Plan { get; }
/// <inheritdoc />
public IUmbracoDatabase Database { get; }
public IUmbracoDatabase Database { get; internal set; }
/// <inheritdoc />
public ISqlContext SqlContext => Database.SqlContext;

View File

@@ -1,14 +1,31 @@
namespace Umbraco.Cms.Infrastructure.Migrations;
using Umbraco.Cms.Infrastructure.Scoping;
namespace Umbraco.Cms.Infrastructure.Migrations;
/// <summary>
/// Base class for creating a migration that does not have a scope provided for it
/// Base class for creating a migration that does not have a scope provided for it.
/// </summary>
/// <remarks>
/// This is just a marker class, and has all the same functionality as the underlying MigrationBase
/// </remarks>
public abstract class UnscopedMigrationBase : MigrationBase
{
protected UnscopedMigrationBase(IMigrationContext context) : base(context)
protected UnscopedMigrationBase(IMigrationContext context)
: base(context)
{
}
/// <summary>
/// <para>Scope the database used by the migration builder.</para>
/// <para>This is used with <see cref="UnscopedMigrationBase"/> when you need to execute something before the scope is created
/// but later need to have your queries scoped in a transaction.</para>
/// </summary>
/// <param name="scope">The scope to get the database from.</param>
/// <exception cref="InvalidOperationException">If the migration is missing or has a malformed MigrationContext, this exception is thrown.</exception>
protected void ScopeDatabase(IScope scope)
{
if (Context is not MigrationContext context)
{
throw new InvalidOperationException("Cannot scope database because context is not a MigrationContext");
}
context.Database = scope.Database;
}
}

View File

@@ -96,5 +96,6 @@ public class UmbracoPlan : MigrationPlan
// To 13.0.0
To<AddPropertyEditorUiAliasColumn>("{419827A0-4FCE-464B-A8F3-247C6092AF55}");
To<MigrateDataTypeConfigurations>("{5F15A1CC-353D-4889-8C7E-F303B4766196}");
To<AddGuidsToUserGroups>("{69E12556-D9B3-493A-8E8A-65EC89FB658D}");
}
}

View File

@@ -0,0 +1,180 @@
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Scoping;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0;
public class AddGuidsToUserGroups : UnscopedMigrationBase
{
private const string NewColumnName = "key";
private readonly IScopeProvider _scopeProvider;
public AddGuidsToUserGroups(IMigrationContext context, IScopeProvider scopeProvider)
: base(context)
{
_scopeProvider = scopeProvider;
}
protected override void Migrate()
{
// SQL server can simply add the column, but for SQLite this won't work,
// so we'll have to create a new table and copy over data.
if (DatabaseType != DatabaseType.SQLite)
{
MigrateSqlServer();
return;
}
MigrateSqlite();
}
private void MigrateSqlServer()
{
using IScope scope = _scopeProvider.CreateScope();
using IDisposable notificationSuppression = scope.Notifications.Suppress();
ScopeDatabase(scope);
var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
AddColumnIfNotExists<UserGroupDto>(columns, NewColumnName);
scope.Complete();
}
private void MigrateSqlite()
{
using IScope scope = _scopeProvider.CreateScope();
using IDisposable notificationSuppression = scope.Notifications.Suppress();
ScopeDatabase(scope);
// If the new column already exists we'll do nothing.
if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName))
{
return;
}
// This isn't pretty,
// But since you cannot alter columns, we have to copy the data over and delete the old table.
// However we cannot do this due to foreign keys, so temporarily disable these keys while migrating.
// This leads to an interesting chicken and egg issue, we have to do this before a transaction
// but in the same connection, since foreign keys will default to "ON" next time we open a connection.
// This means we have to just execute the DB command to end the transaction,
// instead of using CompleteTransaction, since this will end the connection.
Database.Execute("COMMIT;");
// We don't have to worry about re-enabling this since it happens automatically.
Database.Execute("PRAGMA foreign_keys=off;");
Database.Execute("BEGIN TRANSACTION;");
// Now that keys are disabled and we have a transaction, we'll do our migration.
MigrateColumnSqlite();
scope.Complete();
}
private void MigrateColumnSqlite()
{
IEnumerable<UserGroupDto> groups = Database.Fetch<OldUserGroupDto>().Select(x => new UserGroupDto
{
Id = x.Id,
Key = Guid.NewGuid(),
Alias = x.Alias,
Name = x.Name,
DefaultPermissions = x.DefaultPermissions,
CreateDate = x.CreateDate,
UpdateDate = x.UpdateDate,
Icon = x.Icon,
HasAccessToAllLanguages = x.HasAccessToAllLanguages,
StartContentId = x.StartContentId,
StartMediaId = x.StartMediaId,
UserGroup2AppDtos = x.UserGroup2AppDtos,
UserGroup2LanguageDtos = x.UserGroup2LanguageDtos
});
// I realize that this may seem a bit drastic,
// however, since SQLite cannot generate GUIDs we have to load all the user groups into memory to generate it.
// So instead of going through the trouble of creating a new table, copying over data, and then deleting
// We can just drop the table directly and re-create it to add the new column.
Delete.Table(Constants.DatabaseSchema.Tables.UserGroup).Do();
Create.Table<UserGroupDto>().Do();
// We have to insert one at a time to be able to not auto increment the id.
foreach (UserGroupDto group in groups)
{
Database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, group);
}
}
[TableName(Constants.DatabaseSchema.Tables.UserGroup)]
[PrimaryKey("id")]
[ExplicitColumns]
private class OldUserGroupDto
{
public OldUserGroupDto()
{
UserGroup2AppDtos = new List<UserGroup2AppDto>();
UserGroup2LanguageDtos = new List<UserGroup2LanguageDto>();
}
[Column("id")]
[PrimaryKeyColumn(IdentitySeed = 6)]
public int Id { get; set; }
[Column("userGroupAlias")]
[Length(200)]
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupAlias")]
public string? Alias { get; set; }
[Column("userGroupName")]
[Length(200)]
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupName")]
public string? Name { get; set; }
[Column("userGroupDefaultPermissions")]
[Length(50)]
[NullSetting(NullSetting = NullSettings.Null)]
public string? DefaultPermissions { get; set; }
[Column("createDate")]
[NullSetting(NullSetting = NullSettings.NotNull)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; }
[Column("updateDate")]
[NullSetting(NullSetting = NullSettings.NotNull)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime UpdateDate { get; set; }
[Column("icon")]
[NullSetting(NullSetting = NullSettings.Null)]
public string? Icon { get; set; }
[Column("hasAccessToAllLanguages")]
[NullSetting(NullSetting = NullSettings.NotNull)]
public bool HasAccessToAllLanguages { get; set; }
[Column("startContentId")]
[NullSetting(NullSetting = NullSettings.Null)]
[ForeignKey(typeof(NodeDto), Name = "FK_startContentId_umbracoNode_id")]
public int? StartContentId { get; set; }
[Column("startMediaId")]
[NullSetting(NullSetting = NullSettings.Null)]
[ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")]
public int? StartMediaId { get; set; }
[ResultColumn]
[Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")]
public List<UserGroup2AppDto> UserGroup2AppDtos { get; set; }
[ResultColumn]
[Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")]
public List<UserGroup2LanguageDto> UserGroup2LanguageDtos { get; set; }
/// <summary>
/// This is only relevant when this column is included in the results (i.e. GetUserGroupsWithUserCounts).
/// </summary>
[ResultColumn]
public int UserCount { get; set; }
}
}

View File

@@ -20,6 +20,12 @@ public class UserGroupDto
[PrimaryKeyColumn(IdentitySeed = 6)]
public int Id { get; set; }
[Column("key")]
[NullSetting(NullSetting = NullSettings.NotNull)]
[Constraint(Default = SystemMethods.NewGuid)]
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupKey")]
public Guid Key { get; set; }
[Column("userGroupAlias")]
[Length(200)]
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupAlias")]

View File

@@ -110,12 +110,23 @@ internal static class UserFactory
return dto;
}
private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group) =>
new ReadOnlyUserGroup(group.Id, group.Name, group.Icon,
group.StartContentId, group.StartMediaId, group.Alias, group.UserGroup2LanguageDtos.Select(x => x.LanguageId),
private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group)
{
IEnumerable<string> permissions = group.DefaultPermissions is null
? Enumerable.Empty<string>()
: group.DefaultPermissions.ToCharArray().Select(x => x.ToString());
return new ReadOnlyUserGroup(
group.Id,
group.Key,
group.Name,
group.Icon,
group.StartContentId,
group.StartMediaId,
group.Alias,
group.UserGroup2LanguageDtos.Select(x => x.LanguageId),
group.UserGroup2AppDtos.Select(x => x.AppAlias).WhereNotNull().ToArray(),
group.DefaultPermissions == null
? Enumerable.Empty<string>()
: group.DefaultPermissions.ToCharArray().Select(x => x.ToString()),
permissions,
group.HasAccessToAllLanguages);
}
}

View File

@@ -22,6 +22,7 @@ internal static class UserGroupFactory
{
userGroup.DisableChangeTracking();
userGroup.Id = dto.Id;
userGroup.Key = dto.Key;
userGroup.CreateDate = dto.CreateDate;
userGroup.UpdateDate = dto.UpdateDate;
userGroup.StartContentId = dto.StartContentId;
@@ -53,6 +54,7 @@ internal static class UserGroupFactory
{
var dto = new UserGroupDto
{
Key = entity.Key,
Alias = entity.Alias,
DefaultPermissions = entity.Permissions == null ? string.Empty : string.Join(string.Empty, entity.Permissions),
Name = entity.Name,

View File

@@ -19,6 +19,7 @@ public sealed class UserGroupMapper : BaseMapper
protected override void DefineMaps()
{
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.Id), nameof(UserGroupDto.Id));
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.Key), nameof(UserGroupDto.Key));
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.Alias), nameof(UserGroupDto.Alias));
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.Name), nameof(UserGroupDto.Name));
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.Icon), nameof(UserGroupDto.Icon));

View File

@@ -403,7 +403,8 @@ public class UserGroupRepository : EntityRepositoryBase<int, IUserGroup>, IUserG
x => x.Alias,
x => x.DefaultPermissions,
x => x.Name,
x => x.HasAccessToAllLanguages)
x => x.HasAccessToAllLanguages,
x => x.Key)
.AndBy<UserGroup2AppDto>(x => x.AppAlias, x => x.UserGroupId);
protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.UserGroup}.id = @id";

View File

@@ -116,7 +116,7 @@ public class UserEditorAuthorizationHelperTests
{
var currentUser = Mock.Of<IUser>(user => user.Groups == new[]
{
new ReadOnlyUserGroup(1, "CurrentUser", "icon-user", null, null, groupAlias, new int[0], new string[0], new string[0], true),
new ReadOnlyUserGroup(1, Guid.NewGuid(), "CurrentUser", "icon-user", null, null, groupAlias, new int[0], new string[0], new string[0], true),
});
IUser savingUser = null; // This means it is a new created user