Browse Source

Started to migrate to roles.

pull/332/head
Sebastian Stehle 7 years ago
parent
commit
7bf44c3da5
  1. 22
      src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
  2. 6
      src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  3. 12
      src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs
  4. 4
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs
  5. 4
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs
  6. 89
      src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  7. 77
      src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs
  8. 39
      src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  9. 6
      src/Squidex.Domain.Apps.Core.Model/DefaultPermissions.cs
  10. 4
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  11. 6
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  12. 4
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  13. 6
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs
  14. 4
      src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs
  15. 12
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
  16. 12
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  17. 2
      src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs
  18. 9
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  19. 2
      src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs
  20. 3
      src/Squidex.Domain.Apps.Entities/IAppProvider.cs
  21. 5
      src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs
  22. 5
      src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs
  23. 35
      src/Squidex.Infrastructure/Security/Permission.cs
  24. 17
      src/Squidex.Shared/Permissions.cs
  25. 9
      src/Squidex.Shared/Users/UserExtensions.cs
  26. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs
  27. 20
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  28. 7
      src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs
  29. 10
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs
  30. 7
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs
  31. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs
  32. 8
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppClientDto.cs
  33. 5
      src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs
  34. 11
      src/Squidex/Pipeline/AppResolver.cs
  35. 2
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs
  36. 14
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs
  37. 6
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs
  38. 16
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs
  39. 14
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs
  40. 18
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs
  41. 44
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs
  42. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexCommandMiddlewareTests.cs
  43. 2
      tools/Migrate_01/OldEvents/AppClientChanged.cs
  44. 5
      tools/Migrate_01/OldEvents/AppClientPermission.cs
  45. 45
      tools/Migrate_01/OldEvents/AppClientUpdated.cs
  46. 45
      tools/Migrate_01/OldEvents/AppContributorAssigned.cs
  47. 5
      tools/Migrate_01/OldEvents/AppContributorPermission.cs

22
src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs

@ -12,9 +12,9 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
public sealed class AppClient public sealed class AppClient
{ {
private readonly string secret;
private readonly string name; private readonly string name;
private readonly AppClientPermission permission; private readonly string secret;
private readonly string role;
public string Name public string Name
{ {
@ -26,28 +26,28 @@ namespace Squidex.Domain.Apps.Core.Apps
get { return secret; } get { return secret; }
} }
public AppClientPermission Permission public string Role
{ {
get { return permission; } get { return role; }
} }
public AppClient(string name, string secret, AppClientPermission permission) public AppClient(string name, string secret, string role)
{ {
Guard.NotNullOrEmpty(name, nameof(name)); Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNullOrEmpty(secret, nameof(secret)); Guard.NotNullOrEmpty(secret, nameof(secret));
Guard.Enum(permission, nameof(permission)); Guard.NotNullOrEmpty(role, nameof(role));
this.name = name; this.name = name;
this.role = role;
this.secret = secret; this.secret = secret;
this.permission = permission;
} }
[Pure] [Pure]
public AppClient Update(AppClientPermission newPermission) public AppClient Update(string newRole)
{ {
Guard.Enum(newPermission, nameof(newPermission)); Guard.NotNullOrEmpty(newRole, nameof(newRole));
return new AppClient(name, secret, newPermission); return new AppClient(name, secret, newRole);
} }
[Pure] [Pure]
@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
Guard.NotNullOrEmpty(newName, nameof(newName)); Guard.NotNullOrEmpty(newName, nameof(newName));
return new AppClient(newName, secret, permission); return new AppClient(newName, secret, role);
} }
} }
} }

6
src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs

@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
Guard.NotNullOrEmpty(id, nameof(id)); Guard.NotNullOrEmpty(id, nameof(id));
return new AppClients(Inner.Add(id, new AppClient(id, secret, AppClientPermission.Editor))); return new AppClients(Inner.Add(id, new AppClient(id, secret, Role.Editor)));
} }
[Pure] [Pure]
@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Apps
} }
[Pure] [Pure]
public AppClients Update(string id, AppClientPermission permission) public AppClients Update(string id, string role)
{ {
Guard.NotNullOrEmpty(id, nameof(id)); Guard.NotNullOrEmpty(id, nameof(id));
@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
return new AppClients(Inner.SetItem(id, client.Update(permission))); return new AppClients(Inner.SetItem(id, client.Update(role)));
} }
} }
} }

12
src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs

@ -11,27 +11,27 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
public sealed class AppContributors : DictionaryWrapper<string, AppContributorPermission> public sealed class AppContributors : DictionaryWrapper<string, string>
{ {
public static readonly AppContributors Empty = new AppContributors(); public static readonly AppContributors Empty = new AppContributors();
private AppContributors() private AppContributors()
: base(ImmutableDictionary<string, AppContributorPermission>.Empty) : base(ImmutableDictionary<string, string>.Empty)
{ {
} }
public AppContributors(ImmutableDictionary<string, AppContributorPermission> inner) public AppContributors(ImmutableDictionary<string, string> inner)
: base(inner) : base(inner)
{ {
} }
[Pure] [Pure]
public AppContributors Assign(string contributorId, AppContributorPermission permission) public AppContributors Assign(string contributorId, string role)
{ {
Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); Guard.NotNullOrEmpty(contributorId, nameof(contributorId));
Guard.Enum(permission, nameof(permission)); Guard.NotNullOrEmpty(role, nameof(role));
return new AppContributors(Inner.SetItem(contributorId, permission)); return new AppContributors(Inner.SetItem(contributorId, role));
} }
[Pure] [Pure]

4
src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
{ {
protected override void WriteValue(JsonWriter writer, AppContributors value, JsonSerializer serializer) protected override void WriteValue(JsonWriter writer, AppContributors value, JsonSerializer serializer)
{ {
var json = new Dictionary<string, AppContributorPermission>(value.Count); var json = new Dictionary<string, string>(value.Count);
foreach (var contributor in value) foreach (var contributor in value)
{ {
@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
protected override AppContributors ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) protected override AppContributors ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{ {
var json = serializer.Deserialize<Dictionary<string, AppContributorPermission>>(reader); var json = serializer.Deserialize<Dictionary<string, string>>(reader);
return new AppContributors(json.ToImmutableDictionary()); return new AppContributors(json.ToImmutableDictionary());
} }

4
src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
public string Secret { get; set; } public string Secret { get; set; }
[JsonProperty] [JsonProperty]
public AppClientPermission Permission { get; set; } public string Role { get; set; }
public JsonAppClient() public JsonAppClient()
{ {
@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
public AppClient ToClient() public AppClient ToClient()
{ {
return new AppClient(Name, Secret, Permission); return new AppClient(Name, Secret, Role);
} }
} }
} }

89
src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -0,0 +1,89 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using System.Collections.Generic;
using P = Squidex.Shared.Permissions;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class Role
{
public const string Editor = "Editor";
public const string Developer = "Developer";
public const string Owner = "Owner";
public const string Reader = "Reader";
private static readonly HashSet<string> DefaultRolesSet = new HashSet<string>
{
Editor,
Developer,
Owner,
Reader
};
public string Name { get; }
public PermissionSet Permissions { get; }
public Role(string name, PermissionSet permissions)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(permissions, nameof(permissions));
Name = name;
Permissions = permissions;
}
public Role(string name, params Permission[] permissions)
: this(name, new PermissionSet(permissions))
{
}
public static bool IsDefaultRole(string role)
{
return role != null && DefaultRolesSet.Contains(role);
}
public static Role CreateOwner(string app)
{
return new Role(Owner,
P.ForApp(P.App, app));
}
public static Role CreateEditor(string app)
{
return new Role(Editor,
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContents, app),
P.ForApp(P.AppAssets, app));
}
public static Role CreateReader(string app)
{
return new Role(Reader,
P.ForApp(P.AppAssetsRead, app),
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContentsRead, app),
P.ForApp(P.AppContentsGraphQL, app));
}
public static Role CreateDeveloper(string app)
{
return new Role(Developer,
P.ForApp(P.AppApi, app),
P.ForApp(P.AppAssets, app),
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContents, app),
P.ForApp(P.AppPatterns, app),
P.ForApp(P.AppRules, app),
P.ForApp(P.AppSchemas, app));
}
}
}

77
src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs

@ -1,77 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using System.Linq;
namespace Squidex.Domain.Apps.Core.Apps
{
public static class RoleExtension
{
public static string[] ToPermissionIds(this AppClientPermission clientPermission, string app)
{
return clientPermission.ToPermissions(app).ToIds().ToArray();
}
public static string[] ToPermissionIds(this AppContributorPermission contributorPermission, string app)
{
return contributorPermission.ToPermissions(app).ToIds().ToArray();
}
public static PermissionSet ToPermissions(this AppClientPermission clientPermission, string app)
{
Guard.Enum(clientPermission, nameof(clientPermission));
Guard.NotNullOrEmpty(app, nameof(app));
switch (clientPermission)
{
case AppClientPermission.Developer:
return ToPermissions(AppContributorPermission.Developer, app);
case AppClientPermission.Editor:
return ToPermissions(AppContributorPermission.Editor, app);
case AppClientPermission.Reader:
return new PermissionSet(
Permissions.ForApp(Permissions.AppCommon, app),
Permissions.ForApp(Permissions.AppContentsRead, app),
Permissions.ForApp(Permissions.AppContentsGraphQL, app));
}
return PermissionSet.Empty;
}
public static PermissionSet ToPermissions(this AppContributorPermission contributorPermission, string app)
{
Guard.Enum(contributorPermission, nameof(contributorPermission));
Guard.NotNullOrEmpty(app, nameof(app));
switch (contributorPermission)
{
case AppContributorPermission.Owner:
return new PermissionSet(
Permissions.ForApp(Permissions.App, app));
case AppContributorPermission.Developer:
return new PermissionSet(
Permissions.ForApp(Permissions.AppApi, app),
Permissions.ForApp(Permissions.AppAssets, app),
Permissions.ForApp(Permissions.AppCommon, app),
Permissions.ForApp(Permissions.AppContents, app),
Permissions.ForApp(Permissions.AppPatterns, app),
Permissions.ForApp(Permissions.AppRules, app),
Permissions.ForApp(Permissions.AppSchemas, app));
case AppContributorPermission.Editor:
return new PermissionSet(
Permissions.ForApp(Permissions.AppCommon, app),
Permissions.ForApp(Permissions.AppContents, app),
Permissions.ForApp(Permissions.AppAssets, app));
}
return PermissionSet.Empty;
}
}
}

39
src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class Roles : DictionaryWrapper<string, Role>
{
public static readonly Roles Empty = new Roles();
private Roles()
: base(ImmutableDictionary<string, Role>.Empty)
{
}
public Roles(ImmutableDictionary<string, Role> inner)
: base(inner)
{
}
public static Roles CreateDefaults(string app)
{
return new Roles(
new Dictionary<string, Role>
{
[Role.Developer] = Role.CreateDeveloper(app),
[Role.Editor] = Role.CreateEditor(app),
[Role.Owner] = Role.CreateOwner(app),
[Role.Reader] = Role.CreateReader(app)
}.ToImmutableDictionary());
}
}
}

6
src/Squidex.Domain.Apps.Core.Model/DefaultPermissions.cs

@ -1,6 +0,0 @@
namespace Squidex.Domain.Apps.Core
{
public sealed class DefaultPermissions
{
}
}

4
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -20,6 +20,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security;
using Squidex.Shared; using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
@ -159,9 +160,10 @@ namespace Squidex.Domain.Apps.Entities
}); });
} }
public Task<List<IAppEntity>> GetUserApps(string userId, string[] permissions) public Task<List<IAppEntity>> GetUserApps(string userId, PermissionSet permissions)
{ {
Guard.NotNull(userId, nameof(userId)); Guard.NotNull(userId, nameof(userId));
Guard.NotNull(permissions, nameof(permissions));
return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () => return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
{ {

6
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -226,9 +226,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed()));
} }
if (command.Permission.HasValue) if (command.Role != null)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Permission = command.Permission.Value })); RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Role = command.Role }));
} }
} }
@ -327,7 +327,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
private static AppContributorAssigned CreateInitialOwner(RefToken actor) private static AppContributorAssigned CreateInitialOwner(RefToken actor)
{ {
return new AppContributorAssigned { ContributorId = actor.Identifier, Permission = AppContributorPermission.Owner }; return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner };
} }
protected override AppState OnEvent(Envelope<IEvent> @event) protected override AppState OnEvent(Envelope<IEvent> @event)

4
src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
: base(typeNameRegistry) : base(typeNameRegistry)
{ {
AddEventMessage<AppContributorAssigned>( AddEventMessage<AppContributorAssigned>(
"assigned {user:[Contributor]} as {[Permission]}"); "assigned {user:[Contributor]} as {[Role]}");
AddEventMessage<AppContributorRemoved>( AddEventMessage<AppContributorRemoved>(
"removed {user:[Contributor]} from app"); "removed {user:[Contributor]} from app");
@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
return Task.FromResult( return Task.FromResult(
ForEvent(@event, channel) ForEvent(@event, channel)
.AddParameter("Contributor", @event.ContributorId).AddParameter("Permission", @event.Permission)); .AddParameter("Contributor", @event.ContributorId).AddParameter("Role", @event.Role));
} }
protected Task<HistoryEventToStore> On(AppClientAttached @event) protected Task<HistoryEventToStore> On(AppClientAttached @event)

6
src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Apps; using Roles = Squidex.Domain.Apps.Core.Apps.Role;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
@ -13,8 +13,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public string ContributorId { get; set; } public string ContributorId { get; set; }
public bool FromRestore { get; set; } public string Role { get; set; } = Roles.Developer;
public AppContributorPermission Permission { get; set; } public bool FromRestore { get; set; }
} }
} }

4
src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs

@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class UpdateClient : AppCommand public sealed class UpdateClient : AppCommand
@ -15,6 +13,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
public string Name { get; set; } public string Name { get; set; }
public AppClientPermission? Permission { get; set; } public string Role { get; set; }
} }
} }

12
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs

@ -58,14 +58,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
e("Client id is required.", nameof(command.Id)); e("Client id is required.", nameof(command.Id));
} }
if (string.IsNullOrWhiteSpace(command.Name) && command.Permission == null) if (string.IsNullOrWhiteSpace(command.Name) && command.Role == null)
{ {
e("Either name or permission must be defined.", nameof(command.Name), nameof(command.Permission)); e("Either name or role must be defined.", nameof(command.Name), nameof(command.Role));
} }
if (command.Permission.HasValue && !command.Permission.Value.IsEnumValue()) if (command.Role != null && !Role.IsDefaultRole(command.Role))
{ {
e("Permission is not valid.", nameof(command.Permission)); e("Role is not valid.", nameof(command.Role));
} }
if (client == null) if (client == null)
@ -78,9 +78,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
e("Client has already this name.", nameof(command.Name)); e("Client has already this name.", nameof(command.Name));
} }
if (command.Permission == client.Permission) if (command.Role == client.Role)
{ {
e("Client has already this permission.", nameof(command.Permission)); e("Client has already this role.", nameof(command.Role));
} }
}); });
} }

12
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -25,9 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
return Validate.It(() => "Cannot assign contributor.", async e => return Validate.It(() => "Cannot assign contributor.", async e =>
{ {
if (!command.Permission.IsEnumValue()) if (!Role.IsDefaultRole(command.Role))
{ {
e("Permission is not valid.", nameof(command.Permission)); e("Role is not valid.", nameof(command.Role));
} }
if (string.IsNullOrWhiteSpace(command.ContributorId)) if (string.IsNullOrWhiteSpace(command.ContributorId))
@ -47,14 +47,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase) && !command.FromRestore) if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase) && !command.FromRestore)
{ {
throw new SecurityException("You cannot change your own permission."); throw new SecurityException("You cannot change your own role.");
} }
if (contributors.TryGetValue(command.ContributorId, out var existing)) if (contributors.TryGetValue(command.ContributorId, out var existing))
{ {
if (existing == command.Permission) if (existing == command.Role)
{ {
e("Contributor has already this permission.", nameof(command.Permission)); e("Contributor has already this role.", nameof(command.Role));
} }
} }
else if (plan.MaxContributors == contributors.Count) else if (plan.MaxContributors == contributors.Count)
@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
e("Contributor id is required.", nameof(command.ContributorId)); e("Contributor id is required.", nameof(command.ContributorId));
} }
var ownerIds = contributors.Where(x => x.Value == AppContributorPermission.Owner).Select(x => x.Key).ToList(); var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList();
if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId))
{ {

2
src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
{ {
string Name { get; } string Name { get; }
Roles Roles { get; }
AppPlan Plan { get; } AppPlan Plan { get; }
AppClients Clients { get; } AppClients Clients { get; }

9
src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -22,6 +22,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
[JsonProperty] [JsonProperty]
public string Name { get; set; } public string Name { get; set; }
[JsonProperty]
public Roles Roles { get; set; } = Roles.Empty;
[JsonProperty] [JsonProperty]
public AppPlan Plan { get; set; } public AppPlan Plan { get; set; }
@ -42,6 +45,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
protected void On(AppCreated @event) protected void On(AppCreated @event)
{ {
Roles = Roles.CreateDefaults(@event.Name);
SimpleMapper.Map(@event, this); SimpleMapper.Map(@event, this);
} }
@ -52,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
protected void On(AppContributorAssigned @event) protected void On(AppContributorAssigned @event)
{ {
Contributors = Contributors.Assign(@event.ContributorId, @event.Permission); Contributors = Contributors.Assign(@event.ContributorId, @event.Role);
} }
protected void On(AppContributorRemoved @event) protected void On(AppContributorRemoved @event)
@ -67,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
protected void On(AppClientUpdated @event) protected void On(AppClientUpdated @event)
{ {
Clients = Clients.Update(@event.Id, @event.Permission); Clients = Clients.Update(@event.Id, @event.Role);
} }
protected void On(AppClientRenamed @event) protected void On(AppClientRenamed @event)

2
src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs

@ -252,7 +252,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
AppId = CurrentJob.AppId, AppId = CurrentJob.AppId,
ContributorId = actor.Identifier, ContributorId = actor.Identifier,
FromRestore = true, FromRestore = true,
Permission = AppContributorPermission.Developer Role = Role.Developer
}); });
} }

3
src/Squidex.Domain.Apps.Entities/IAppProvider.cs

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
{ {
@ -28,6 +29,6 @@ namespace Squidex.Domain.Apps.Entities
Task<List<IRuleEntity>> GetRulesAsync(Guid appId); Task<List<IRuleEntity>> GetRulesAsync(Guid appId);
Task<List<IAppEntity>> GetUserApps(string userId, string[] permissions); Task<List<IAppEntity>> GetUserApps(string userId, PermissionSet permissions);
} }
} }

5
src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs

@ -5,16 +5,15 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps namespace Squidex.Domain.Apps.Events.Apps
{ {
[EventType(nameof(AppClientUpdated))] [EventType(nameof(AppClientUpdated), 2)]
public sealed class AppClientUpdated : AppEvent public sealed class AppClientUpdated : AppEvent
{ {
public string Id { get; set; } public string Id { get; set; }
public AppClientPermission Permission { get; set; } public string Role { get; set; }
} }
} }

5
src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs

@ -5,16 +5,15 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps namespace Squidex.Domain.Apps.Events.Apps
{ {
[EventType(nameof(AppContributorAssigned))] [EventType(nameof(AppContributorAssigned), 2)]
public sealed class AppContributorAssigned : AppEvent public sealed class AppContributorAssigned : AppEvent
{ {
public string ContributorId { get; set; } public string ContributorId { get; set; }
public AppContributorPermission Permission { get; set; } public string Role { get; set; }
} }
} }

35
src/Squidex.Infrastructure/Security/Permission.cs

@ -18,7 +18,7 @@ namespace Squidex.Infrastructure.Security
private static readonly char[] AlternativeSeparators = { '|' }; private static readonly char[] AlternativeSeparators = { '|' };
private readonly string description; private readonly string description;
private readonly string id; private readonly string id;
private readonly HashSet<string>[] idParts; private readonly Lazy<HashSet<string>[]> idParts;
public string Id public string Id
{ {
@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.Security
this.id = id; this.id = id;
idParts = id idParts = new Lazy<HashSet<string>[]>(() => id
.Split(MainSeparators, StringSplitOptions.RemoveEmptyEntries) .Split(MainSeparators, StringSplitOptions.RemoveEmptyEntries)
.Select(x => .Select(x =>
{ {
@ -51,7 +51,7 @@ namespace Squidex.Infrastructure.Security
return new HashSet<string>(alternatives, StringComparer.OrdinalIgnoreCase); return new HashSet<string>(alternatives, StringComparer.OrdinalIgnoreCase);
}) })
.ToArray(); .ToArray());
} }
public bool Allows(Permission permission) public bool Allows(Permission permission)
@ -61,17 +61,20 @@ namespace Squidex.Infrastructure.Security
return false; return false;
} }
if (idParts.Length > permission.idParts.Length) var lhs = idParts.Value;
var rhs = permission.idParts.Value;
if (lhs.Length > rhs.Length)
{ {
return false; return false;
} }
for (var i = 0; i < idParts.Length; i++) for (var i = 0; i < lhs.Length; i++)
{ {
var lhs = idParts[i]; var l = lhs[i];
var rhs = permission.idParts[i]; var r = rhs[i];
if (lhs != null && (rhs == null || !lhs.Intersect(rhs).Any())) if (l != null && (r == null || !l.Intersect(r).Any()))
{ {
return false; return false;
} }
@ -87,12 +90,15 @@ namespace Squidex.Infrastructure.Security
return false; return false;
} }
for (var i = 0; i < Math.Min(idParts.Length, permission.idParts.Length); i++) var lhs = idParts.Value;
var rhs = permission.idParts.Value;
for (var i = 0; i < Math.Min(lhs.Length, rhs.Length); i++)
{ {
var lhs = idParts[i]; var l = lhs[i];
var rhs = permission.idParts[i]; var r = rhs[i];
if (lhs != null && rhs != null && !lhs.Intersect(rhs).Any()) if (l != null && r != null && !l.Intersect(r).Any())
{ {
return false; return false;
} }
@ -101,6 +107,11 @@ namespace Squidex.Infrastructure.Security
return true; return true;
} }
public bool StartsWith(string id)
{
return id.StartsWith(id, StringComparison.OrdinalIgnoreCase);
}
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
return Equals(obj as Permission); return Equals(obj as Permission);

17
src/Squidex.Shared/Permissions.cs

@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
@ -114,19 +112,20 @@ namespace Squidex.Shared
return new Permission(id.Replace("{app}", app ?? "*").Replace("{name}", schema ?? "*")); return new Permission(id.Replace("{app}", app ?? "*").Replace("{name}", schema ?? "*"));
} }
public static string[] ToAppPermissionIds(this IEnumerable<string> permissions, string app) public static PermissionSet ToAppPermissions(this PermissionSet permissions, string app)
{ {
var result = permissions.Where(x => x.StartsWith($"squidex.apps.{app}", StringComparison.OrdinalIgnoreCase)).ToArray(); var matching = permissions.Where(x => x.StartsWith($"squidex.apps.{app}"));
return result; return new PermissionSet(matching);
} }
public static string[] ToAppNames(this IEnumerable<string> permissions) public static string[] ToAppNames(this PermissionSet permissions)
{ {
var matching = permissions.Where(x => x.StartsWith($"squidex.apps."));
var result = var result =
permissions matching
.Where(x => x.StartsWith("squidex.apps.", StringComparison.OrdinalIgnoreCase)) .Select(x => x.Id.Split('.'))
.Select(x => x.Split('.'))
.Select(x => x[2]) .Select(x => x[2])
.Distinct() .Distinct()
.ToArray(); .ToArray();

9
src/Squidex.Shared/Users/UserExtensions.cs

@ -9,6 +9,7 @@ using System;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
namespace Squidex.Shared.Users namespace Squidex.Shared.Users
@ -100,14 +101,14 @@ namespace Squidex.Shared.Users
return user.GetClaimValue(SquidexClaimTypes.DisplayName); return user.GetClaimValue(SquidexClaimTypes.DisplayName);
} }
public static string[] Permissions(this ClaimsPrincipal principal) public static PermissionSet Permissions(this ClaimsPrincipal principal)
{ {
return principal.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).Select(x => x.Value).ToArray(); return new PermissionSet(principal.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).Select(x => new Permission(x.Value)));
} }
public static string[] Permissions(this IUser user) public static PermissionSet Permissions(this IUser user)
{ {
return user.GetClaimValues(SquidexClaimTypes.Permissions); return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x)));
} }
public static string GetClaimValue(this IUser user, string type) public static string GetClaimValue(this IUser user, string type)

2
src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs

@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
var response = new AppCreatedDto var response = new AppCreatedDto
{ {
Id = result.IdOrValue.ToString(), Id = result.IdOrValue.ToString(),
Permissions = AppContributorPermission.Owner.ToPermissions(name).Select(x => x.Id).ToArray(), Permissions = Role.CreateOwner(name).Permissions.ToIds().ToArray(),
PlanName = apps.GetPlan(null)?.Name, PlanName = apps.GetPlan(null)?.Name,
PlanUpgrade = apps.GetPlanUpgrade(null)?.Name, PlanUpgrade = apps.GetPlanUpgrade(null)?.Name,
Version = result.Version Version = result.Version

20
src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -6,13 +6,15 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared; using Squidex.Shared;
@ -62,19 +64,23 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
public string PlanUpgrade { get; set; } public string PlanUpgrade { get; set; }
public static AppDto FromApp(IAppEntity app, string userId, string[] permissions, IAppPlansProvider plans) public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans)
{ {
var response = SimpleMapper.Map(app, new AppDto()); var permissions = new List<Permission>();
if (app.Contributors.TryGetValue(userId, out var role)) if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGetValue(roleName, out var role))
{ {
response.Permissions = role.ToPermissionIds(app.Name); permissions.AddRange(role.Permissions);
} }
else
if (userPermissions != null)
{ {
response.Permissions = permissions.ToAppPermissionIds(app.Name); permissions.AddRange(userPermissions.ToAppPermissions(app.Name));
} }
var response = SimpleMapper.Map(app, new AppDto());
response.Permissions = permissions.Select(x => x.Id).ToArray();
response.PlanName = plans.GetPlanForApp(app)?.Name; response.PlanName = plans.GetPlanForApp(app)?.Name;
response.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name; response.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;

7
src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs

@ -7,8 +7,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -23,10 +21,9 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public string ContributorId { get; set; } public string ContributorId { get; set; }
/// <summary> /// <summary>
/// The permission level as a contributor. /// The role of the contributor.
/// </summary> /// </summary>
[JsonConverter(typeof(StringEnumConverter))] public string Role { get; set; }
public AppContributorPermission Permission { get; set; }
public AssignContributor ToCommand() public AssignContributor ToCommand()
{ {

10
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs

@ -8,10 +8,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Roles = Squidex.Domain.Apps.Core.Apps.Role;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -36,11 +36,9 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// The permissions of the client. /// The role of the client.
/// </summary> /// </summary>
[Required] public string Role { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public AppClientPermission Permission { get; set; }
public static ClientDto FromKvp(KeyValuePair<string, AppClient> kvp) public static ClientDto FromKvp(KeyValuePair<string, AppClient> kvp)
{ {
@ -49,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public static ClientDto FromCommand(AttachClient command) public static ClientDto FromCommand(AttachClient command)
{ {
return SimpleMapper.Map(command, new ClientDto { Name = command.Id, Permission = AppClientPermission.Editor }); return SimpleMapper.Map(command, new ClientDto { Name = command.Id, Role = Roles.Editor });
} }
} }
} }

7
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs

@ -7,8 +7,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -21,9 +19,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public string ContributorId { get; set; } public string ContributorId { get; set; }
/// <summary> /// <summary>
/// The permission level as a contributor. /// The role of the contributor.
/// </summary> /// </summary>
[JsonConverter(typeof(StringEnumConverter))] public string Role { get; set; }
public AppContributorPermission Permission { get; set; }
} }
} }

2
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs

@ -29,7 +29,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
var plan = plans.GetPlanForApp(app); var plan = plans.GetPlanForApp(app);
var contributors = app.Contributors.Select(x => new ContributorDto { ContributorId = x.Key, Permission = x.Value }).ToArray(); var contributors = app.Contributors.Select(x => new ContributorDto { ContributorId = x.Key, Role = x.Value }).ToArray();
return new ContributorsDto { Contributors = contributors, MaxContributors = plan.MaxContributors }; return new ContributorsDto { Contributors = contributors, MaxContributors = plan.MaxContributors };
} }

8
src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppClientDto.cs

@ -6,9 +6,6 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -23,10 +20,9 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// The permissions of the client. /// The role of the client.
/// </summary> /// </summary>
[JsonConverter(typeof(StringEnumConverter))] public string Role { get; set; }
public AppClientPermission? Permission { get; set; }
public UpdateClient ToCommand(string clientId) public UpdateClient ToCommand(string clientId)
{ {

5
src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Users; using Squidex.Shared.Users;
@ -45,7 +46,9 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
public static UserDto FromUser(IUser user) public static UserDto FromUser(IUser user)
{ {
return SimpleMapper.Map(user, new UserDto { DisplayName = user.DisplayName(), Permissions = user.Permissions() }); var permissions = user.Permissions().ToIds().ToArray();
return SimpleMapper.Map(user, new UserDto { DisplayName = user.DisplayName(), Permissions = permissions });
} }
} }
} }

11
src/Squidex/Pipeline/AppResolver.cs

@ -10,7 +10,6 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
@ -61,7 +60,7 @@ namespace Squidex.Pipeline
if (permissions.Count == 0) if (permissions.Count == 0)
{ {
var set = new PermissionSet(user.Permissions().Select(x => new Permission(x))); var set = user.Permissions();
if (!set.Includes(Permissions.ForApp(Permissions.App, appName))) if (!set.Includes(Permissions.ForApp(Permissions.App, appName)))
{ {
@ -87,9 +86,9 @@ namespace Squidex.Pipeline
{ {
var clientId = user.GetClientId(); var clientId = user.GetClientId();
if (clientId != null && app.Clients.TryGetValue(clientId, out var client)) if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGetValue(client.Role, out var role))
{ {
return client.Permission.ToPermissions(app.Name); return role.Permissions;
} }
return PermissionSet.Empty; return PermissionSet.Empty;
@ -99,9 +98,9 @@ namespace Squidex.Pipeline
{ {
var subjectId = user.OpenIdSubject(); var subjectId = user.OpenIdSubject();
if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var permission)) if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGetValue(roleName, out var role))
{ {
return permission.ToPermissions(app.Name); return role.Permissions;
} }
return PermissionSet.Empty; return PermissionSet.Empty;

2
tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs

@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
clients = clients.Add("3", "my-secret"); clients = clients.Add("3", "my-secret");
clients = clients.Add("4", "my-secret"); clients = clients.Add("4", "my-secret");
clients = clients.Update("3", AppClientPermission.Editor); clients = clients.Update("3", Role.Editor);
clients = clients.Rename("3", "My Client 3"); clients = clients.Rename("3", "My Client 3");
clients = clients.Rename("2", "My Client 2"); clients = clients.Rename("2", "My Client 2");

14
tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs

@ -23,15 +23,15 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{ {
var clients_1 = clients_0.Add("2", "my-secret"); var clients_1 = clients_0.Add("2", "my-secret");
clients_1["2"].Should().BeEquivalentTo(new AppClient("2", "my-secret", AppClientPermission.Editor)); clients_1["2"].Should().BeEquivalentTo(new AppClient("2", "my-secret", Role.Editor));
} }
[Fact] [Fact]
public void Should_assign_clients_with_permission() public void Should_assign_clients_with_permission()
{ {
var clients_1 = clients_0.Add("2", new AppClient("my-name", "my-secret", AppClientPermission.Reader)); var clients_1 = clients_0.Add("2", new AppClient("my-name", "my-secret", Role.Reader));
clients_1["2"].Should().BeEquivalentTo(new AppClient("my-name", "my-secret", AppClientPermission.Reader)); clients_1["2"].Should().BeEquivalentTo(new AppClient("my-name", "my-secret", Role.Reader));
} }
[Fact] [Fact]
@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{ {
var clients_1 = clients_0.Rename("1", "new-name"); var clients_1 = clients_0.Rename("1", "new-name");
clients_1["1"].Should().BeEquivalentTo(new AppClient("new-name", "my-secret", AppClientPermission.Editor)); clients_1["1"].Should().BeEquivalentTo(new AppClient("new-name", "my-secret", Role.Editor));
} }
[Fact] [Fact]
@ -61,15 +61,15 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact] [Fact]
public void Should_update_client() public void Should_update_client()
{ {
var client_1 = clients_0.Update("1", AppClientPermission.Reader); var client_1 = clients_0.Update("1", Role.Reader);
client_1["1"].Should().BeEquivalentTo(new AppClient("1", "my-secret", AppClientPermission.Reader)); client_1["1"].Should().BeEquivalentTo(new AppClient("1", "my-secret", Role.Reader));
} }
[Fact] [Fact]
public void Should_return_same_clients_if_client_to_update_not_found() public void Should_return_same_clients_if_client_to_update_not_found()
{ {
var clients_1 = clients_0.Update("2", AppClientPermission.Reader); var clients_1 = clients_0.Update("2", Role.Reader);
Assert.Same(clients_0, clients_1); Assert.Same(clients_0, clients_1);
} }

6
tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs

@ -22,9 +22,9 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{ {
var contributors = AppContributors.Empty; var contributors = AppContributors.Empty;
contributors = contributors.Assign("1", AppContributorPermission.Developer); contributors = contributors.Assign("1", Role.Developer);
contributors = contributors.Assign("2", AppContributorPermission.Editor); contributors = contributors.Assign("2", Role.Editor);
contributors = contributors.Assign("3", AppContributorPermission.Owner); contributors = contributors.Assign("3", Role.Owner);
var serialized = JToken.FromObject(contributors, serializer).ToObject<AppContributors>(serializer); var serialized = JToken.FromObject(contributors, serializer).ToObject<AppContributors>(serializer);

16
tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs

@ -19,26 +19,26 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact] [Fact]
public void Should_assign_new_contributor() public void Should_assign_new_contributor()
{ {
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Developer); var contributors_1 = contributors_0.Assign("1", Role.Developer);
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); var contributors_2 = contributors_1.Assign("2", Role.Editor);
Assert.Equal(AppContributorPermission.Developer, contributors_2["1"]); Assert.Equal(Role.Developer, contributors_2["1"]);
Assert.Equal(AppContributorPermission.Editor, contributors_2["2"]); Assert.Equal(Role.Editor, contributors_2["2"]);
} }
[Fact] [Fact]
public void Should_replace_contributor_if_already_exists() public void Should_replace_contributor_if_already_exists()
{ {
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Developer); var contributors_1 = contributors_0.Assign("1", Role.Developer);
var contributors_2 = contributors_1.Assign("1", AppContributorPermission.Owner); var contributors_2 = contributors_1.Assign("1", Role.Owner);
Assert.Equal(AppContributorPermission.Owner, contributors_2["1"]); Assert.Equal(Role.Owner, contributors_2["1"]);
} }
[Fact] [Fact]
public void Should_remove_contributor() public void Should_remove_contributor()
{ {
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Developer); var contributors_1 = contributors_0.Assign("1", Role.Developer);
var contributors_2 = contributors_1.Remove("1"); var contributors_2 = contributors_1.Remove("1");
Assert.Empty(contributors_2); Assert.Empty(contributors_2);

14
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs

@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppCreated { Name = AppName }), CreateEvent(new AppCreated { Name = AppName }),
CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }), CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Role = Role.Owner }),
CreateEvent(new AppLanguageAdded { Language = Language.EN }), CreateEvent(new AppLanguageAdded { Language = Language.EN }),
CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }), CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }),
CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" }) CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" })
@ -158,7 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
[Fact] [Fact]
public async Task AssignContributor_should_create_events_and_update_state() public async Task AssignContributor_should_create_events_and_update_state()
{ {
var command = new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor }; var command = new AssignContributor { ContributorId = contributorId, Role = Role.Editor };
await ExecuteCreateAsync(); await ExecuteCreateAsync();
@ -166,11 +166,11 @@ namespace Squidex.Domain.Apps.Entities.Apps
result.ShouldBeEquivalent(EntityCreatedResult.Create(contributorId, 5)); result.ShouldBeEquivalent(EntityCreatedResult.Create(contributorId, 5));
Assert.Equal(AppContributorPermission.Editor, sut.Snapshot.Contributors[contributorId]); Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor }) CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Editor })
); );
} }
@ -236,7 +236,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
[Fact] [Fact]
public async Task UpdateClient_should_create_events_and_update_state() public async Task UpdateClient_should_create_events_and_update_state()
{ {
var command = new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer }; var command = new UpdateClient { Id = clientId, Name = clientNewName, Role = Role.Developer };
await ExecuteCreateAsync(); await ExecuteCreateAsync();
await ExecuteAttachClientAsync(); await ExecuteAttachClientAsync();
@ -250,7 +250,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }),
CreateEvent(new AppClientUpdated { Id = clientId, Permission = AppClientPermission.Developer }) CreateEvent(new AppClientUpdated { Id = clientId, Role = Role.Developer })
); );
} }
@ -402,7 +402,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
private Task ExecuteAssignContributorAsync() private Task ExecuteAssignContributorAsync()
{ {
return sut.ExecuteAsync(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); return sut.ExecuteAsync(CreateCommand(new AssignContributor { ContributorId = contributorId, Role = Role.Editor }));
} }
private Task ExecuteAttachClientAsync() private Task ExecuteAttachClientAsync()

18
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs

@ -94,25 +94,25 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
} }
[Fact] [Fact]
public void UpdateClient_should_throw_exception_if_client_has_no_name_and_permission() public void UpdateClient_should_throw_exception_if_client_has_no_name_and_role()
{ {
var command = new UpdateClient { Id = "ios" }; var command = new UpdateClient { Id = "ios" };
var clients_1 = clients_0.Add("ios", "secret"); var clients_1 = clients_0.Add("ios", "secret");
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command), ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command),
new ValidationError("Either name or permission must be defined.", "Name", "Permission")); new ValidationError("Either name or role must be defined.", "Name", "Role"));
} }
[Fact] [Fact]
public void UpdateClient_should_throw_exception_if_client_has_invalid_permission() public void UpdateClient_should_throw_exception_if_client_has_invalid_role()
{ {
var command = new UpdateClient { Id = "ios", Permission = (AppClientPermission)10 }; var command = new UpdateClient { Id = "ios", Role = "Invalid" };
var clients_1 = clients_0.Add("ios", "secret"); var clients_1 = clients_0.Add("ios", "secret");
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command), ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command),
new ValidationError("Permission is not valid.", "Permission")); new ValidationError("Role is not valid.", "Role"));
} }
[Fact] [Fact]
@ -127,20 +127,20 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
} }
[Fact] [Fact]
public void UpdateClient_should_throw_exception_if_client_has_same_permission() public void UpdateClient_should_throw_exception_if_client_has_same_role()
{ {
var command = new UpdateClient { Id = "ios", Permission = AppClientPermission.Editor }; var command = new UpdateClient { Id = "ios", Role = Role.Editor };
var clients_1 = clients_0.Add("ios", "secret"); var clients_1 = clients_0.Add("ios", "secret");
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command), ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command),
new ValidationError("Client has already this permission.", "Permission")); new ValidationError("Client has already this role.", "Role"));
} }
[Fact] [Fact]
public void UpdateClient_should_not_throw_exception_if_command_is_valid() public void UpdateClient_should_not_throw_exception_if_command_is_valid()
{ {
var command = new UpdateClient { Id = "ios", Name = "iOS", Permission = AppClientPermission.Reader }; var command = new UpdateClient { Id = "ios", Name = "iOS", Role = Role.Reader };
var clients_1 = clients_0.Add("ios", "secret"); var clients_1 = clients_0.Add("ios", "secret");

44
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs

@ -60,29 +60,29 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
} }
[Fact] [Fact]
public Task CanAssign_should_throw_exception_if_permission_not_valid() public Task CanAssign_should_throw_exception_if_role_not_valid()
{ {
var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; var command = new AssignContributor { ContributorId = "1", Role = "Invalid" };
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan), return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan),
new ValidationError("Permission is not valid.", "Permission")); new ValidationError("Role is not valid.", "Role"));
} }
[Fact] [Fact]
public Task CanAssign_should_throw_exception_if_user_already_exists_with_same_permission() public Task CanAssign_should_throw_exception_if_user_already_exists_with_same_role()
{ {
var command = new AssignContributor { ContributorId = "1" }; var command = new AssignContributor { ContributorId = "1", Role = Role.Owner };
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); var contributors_1 = contributors_0.Assign("1", Role.Owner);
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, command, users, appPlan), return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, command, users, appPlan),
new ValidationError("Contributor has already this permission.", "Permission")); new ValidationError("Contributor has already this role.", "Role"));
} }
[Fact] [Fact]
public Task CanAssign_should_throw_exception_if_user_not_found() public Task CanAssign_should_throw_exception_if_user_not_found()
{ {
var command = new AssignContributor { ContributorId = "notfound", Permission = (AppContributorPermission)10 }; var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner };
return Assert.ThrowsAsync<DomainObjectNotFoundException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); return Assert.ThrowsAsync<DomainObjectNotFoundException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
} }
@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact] [Fact]
public Task CanAssign_should_throw_exception_if_user_is_actor() public Task CanAssign_should_throw_exception_if_user_is_actor()
{ {
var command = new AssignContributor { ContributorId = "3", Permission = AppContributorPermission.Editor, Actor = new RefToken("user", "3") }; var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = new RefToken("user", "3") };
return Assert.ThrowsAsync<SecurityException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); return Assert.ThrowsAsync<SecurityException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
} }
@ -103,8 +103,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var command = new AssignContributor { ContributorId = "3" }; var command = new AssignContributor { ContributorId = "3" };
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); var contributors_1 = contributors_0.Assign("1", Role.Owner);
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); var contributors_2 = contributors_1.Assign("2", Role.Editor);
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan), return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan),
new ValidationError("You have reached the maximum number of contributors for your plan.")); new ValidationError("You have reached the maximum number of contributors for your plan."));
@ -129,25 +129,25 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
} }
[Fact] [Fact]
public Task CanAssign_should_not_throw_exception_if_contributor_has_another_permission() public Task CanAssign_should_not_throw_exception_if_contributor_has_another_role()
{ {
var command = new AssignContributor { ContributorId = "1" }; var command = new AssignContributor { ContributorId = "1", Role = Role.Developer };
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Editor); var contributors_1 = contributors_0.Assign("1", Role.Editor);
return GuardAppContributors.CanAssign(contributors_1, command, users, appPlan); return GuardAppContributors.CanAssign(contributors_1, command, users, appPlan);
} }
[Fact] [Fact]
public Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_permission_changed() public Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed()
{ {
A.CallTo(() => appPlan.MaxContributors) A.CallTo(() => appPlan.MaxContributors)
.Returns(2); .Returns(2);
var command = new AssignContributor { ContributorId = "1" }; var command = new AssignContributor { ContributorId = "1", Role = Role.Developer };
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Editor); var contributors_1 = contributors_0.Assign("1", Role.Editor);
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); var contributors_2 = contributors_1.Assign("2", Role.Editor);
return GuardAppContributors.CanAssign(contributors_2, command, users, appPlan); return GuardAppContributors.CanAssign(contributors_2, command, users, appPlan);
} }
@ -174,8 +174,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
var command = new RemoveContributor { ContributorId = "1" }; var command = new RemoveContributor { ContributorId = "1" };
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); var contributors_1 = contributors_0.Assign("1", Role.Owner);
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); var contributors_2 = contributors_1.Assign("2", Role.Editor);
ValidationAssert.Throws(() => GuardAppContributors.CanRemove(contributors_2, command), ValidationAssert.Throws(() => GuardAppContributors.CanRemove(contributors_2, command),
new ValidationError("Cannot remove the only owner.")); new ValidationError("Cannot remove the only owner."));
@ -186,8 +186,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
var command = new RemoveContributor { ContributorId = "1" }; var command = new RemoveContributor { ContributorId = "1" };
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); var contributors_1 = contributors_0.Assign("1", Role.Owner);
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Owner); var contributors_2 = contributors_1.Assign("2", Role.Owner);
GuardAppContributors.CanRemove(contributors_2, command); GuardAppContributors.CanRemove(contributors_2, command);
} }

2
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexCommandMiddlewareTests.cs

@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
.Returns(J.AsTask(appState)); .Returns(J.AsTask(appState));
A.CallTo(() => appState.Contributors) A.CallTo(() => appState.Contributors)
.Returns(AppContributors.Empty.Assign(userId, AppContributorPermission.Owner)); .Returns(AppContributors.Empty.Assign(userId, Role.Owner));
var context = var context =
new CommandContext(new ArchiveApp { AppId = appId }, commandBus) new CommandContext(new ArchiveApp { AppId = appId }, commandBus)

2
tools/Migrate_01/OldEvents/AppClientChanged.cs

@ -6,9 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;

5
src/Squidex.Domain.Apps.Core.Model/Apps/AppClientPermission.cs → tools/Migrate_01/OldEvents/AppClientPermission.cs

@ -5,8 +5,11 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
namespace Squidex.Domain.Apps.Core.Apps using System;
namespace Migrate_01.OldEvents
{ {
[Obsolete]
public enum AppClientPermission public enum AppClientPermission
{ {
Developer, Developer,

45
tools/Migrate_01/OldEvents/AppClientUpdated.cs

@ -0,0 +1,45 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using AppClientUpdatedV2 = Squidex.Domain.Apps.Events.Apps.AppClientUpdated;
namespace Migrate_01.OldEvents
{
[EventType(nameof(AppClientUpdated))]
[Obsolete]
public sealed class AppClientUpdated : AppEvent, IMigratedEvent
{
public string Id { get; set; }
public AppClientPermission Permission { get; set; }
public IEvent Migrate()
{
var result = SimpleMapper.Map(this, new AppClientUpdatedV2());
switch (Permission)
{
case AppClientPermission.Developer:
result.Role = Role.Developer;
break;
case AppClientPermission.Editor:
result.Role = Role.Editor;
break;
case AppClientPermission.Reader:
result.Role = Role.Reader;
break;
}
return result;
}
}
}

45
tools/Migrate_01/OldEvents/AppContributorAssigned.cs

@ -0,0 +1,45 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using AppContributorAssignedV2 = Squidex.Domain.Apps.Events.Apps.AppContributorAssigned;
namespace Migrate_01.OldEvents
{
[EventType(nameof(AppContributorAssigned))]
[Obsolete]
public sealed class AppContributorAssigned : AppEvent, IMigratedEvent
{
public string ContributorId { get; set; }
public AppContributorPermission Permission { get; set; }
public IEvent Migrate()
{
var result = SimpleMapper.Map(this, new AppContributorAssignedV2());
switch (Permission)
{
case AppContributorPermission.Owner:
result.Role = Role.Owner;
break;
case AppContributorPermission.Developer:
result.Role = Role.Developer;
break;
case AppContributorPermission.Editor:
result.Role = Role.Editor;
break;
}
return result;
}
}
}

5
src/Squidex.Domain.Apps.Core.Model/Apps/AppContributorPermission.cs → tools/Migrate_01/OldEvents/AppContributorPermission.cs

@ -5,8 +5,11 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
namespace Squidex.Domain.Apps.Core.Apps using System;
namespace Migrate_01.OldEvents
{ {
[Obsolete]
public enum AppContributorPermission public enum AppContributorPermission
{ {
Owner, Owner,
Loading…
Cancel
Save