diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs index 04ce7605d..4649126c3 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs @@ -4,6 +4,7 @@ // Copyright (c) Squidex UG (haftungsbeschränkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== + using Newtonsoft.Json; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs index 2a7be22e2..2f960353d 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs @@ -18,11 +18,11 @@ namespace Squidex.Domain.Apps.Core.Apps.Json { protected override void WriteValue(JsonWriter writer, Roles value, JsonSerializer serializer) { - var json = new Dictionary(value.Count); + var json = new Dictionary(value.CustomCount); - foreach (var role in value) + foreach (var role in value.Custom) { - json.Add(role.Key, role.Value.Permissions.ToIds().ToArray()); + json.Add(role.Name, role.Permissions.ToIds().ToArray()); } serializer.Serialize(writer, json); @@ -32,7 +32,12 @@ namespace Squidex.Domain.Apps.Core.Apps.Json { var json = serializer.Deserialize>(reader); - return new Roles(json.Select(Convert).ToArray()); + if (json.Count == 0) + { + return Roles.Empty; + } + + return new Roles(json.Select(Convert)); } private static KeyValuePair Convert(KeyValuePair kvp) diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs index 22f6f1b73..1279367c1 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs @@ -8,9 +8,10 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; +using System.Linq; using Squidex.Infrastructure; using Squidex.Infrastructure.Security; -using P = Squidex.Shared.Permissions; +using AllPermissions = Squidex.Shared.Permissions; namespace Squidex.Domain.Apps.Core.Apps { @@ -21,16 +22,13 @@ namespace Squidex.Domain.Apps.Core.Apps public const string Owner = "Owner"; public const string Reader = "Reader"; - private static readonly HashSet DefaultRolesSet = new HashSet(StringComparer.OrdinalIgnoreCase) - { - Editor, - Developer, - Owner, - Reader - }; - public PermissionSet Permissions { get; } + public bool IsDefault + { + get { return Roles.IsDefault(this); } + } + public Role(string name, PermissionSet permissions) : base(name) { @@ -39,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.Apps Permissions = permissions; } - public Role(string name, params Permission[] permissions) + public Role(string name, params string[] permissions) : this(name, new PermissionSet(permissions)) { } @@ -50,50 +48,29 @@ namespace Squidex.Domain.Apps.Core.Apps return new Role(Name, new PermissionSet(permissions)); } - public static bool IsDefaultRole(string role) - { - return role != null && DefaultRolesSet.Contains(role); - } - - public static bool IsRole(string name, string expected) + public bool Equals(string name) { - return name != null && string.Equals(name, expected, StringComparison.OrdinalIgnoreCase); + return name != null && name.Equals(Name, StringComparison.Ordinal); } - public static Role CreateOwner(string app) + public Role ForApp(string app) { - return new Role(Owner, - P.ForApp(P.App, app)); - } + var result = new HashSet + { + AllPermissions.ForApp(AllPermissions.AppCommon, app) + }; - public static Role CreateEditor(string app) - { - return new Role(Editor, - P.ForApp(P.AppAssets, app), - P.ForApp(P.AppCommon, app), - P.ForApp(P.AppContents, app), - P.ForApp(P.AppWorkflowsRead, app)); - } + if (Permissions.Any()) + { + var prefix = AllPermissions.ForApp(AllPermissions.App, app).Id; - 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)); - } + foreach (var permission in Permissions) + { + result.Add(new Permission(string.Concat(prefix, ".", permission.Id))); + } + } - 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.AppWorkflows, app), - P.ForApp(P.AppRules, app), - P.ForApp(P.AppSchemas, app)); + return new Role(Name, new PermissionSet(result)); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs index 4e3e1d066..fa717107a 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs @@ -11,26 +11,78 @@ using System.Diagnostics.Contracts; using System.Linq; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Security; +using Squidex.Shared; namespace Squidex.Domain.Apps.Core.Apps { - public sealed class Roles : ArrayDictionary + public sealed class Roles { - public static readonly Roles Empty = new Roles(); + private readonly ArrayDictionary inner; - private Roles() + public static readonly IReadOnlyDictionary Defaults = new Dictionary { + [Role.Owner] = + new Role(Role.Owner, new PermissionSet( + Clean(Permissions.App))), + [Role.Reader] = + new Role(Role.Reader, new PermissionSet( + Clean(Permissions.AppAssetsRead), + Clean(Permissions.AppContentsRead))), + [Role.Editor] = + new Role(Role.Editor, new PermissionSet( + Clean(Permissions.AppAssets), + Clean(Permissions.AppContents), + Clean(Permissions.AppRolesRead), + Clean(Permissions.AppWorkflowsRead))), + [Role.Developer] = + new Role(Role.Developer, new PermissionSet( + Clean(Permissions.AppApi), + Clean(Permissions.AppAssets), + Clean(Permissions.AppContents), + Clean(Permissions.AppPatterns), + Clean(Permissions.AppRolesRead), + Clean(Permissions.AppRules), + Clean(Permissions.AppSchemas), + Clean(Permissions.AppWorkflows))) + }; + + public static readonly Roles Empty = new Roles(new ArrayDictionary()); + + public int CustomCount + { + get { return inner.Count; } + } + + public Role this[string name] + { + get { return inner[name]; } + } + + public IEnumerable Custom + { + get { return inner.Values; } + } + + public IEnumerable All + { + get { return inner.Values.Union(Defaults.Values); } } - public Roles(KeyValuePair[] items) - : base(items) + private Roles(ArrayDictionary roles) { + inner = roles; + } + + public Roles(IEnumerable> items) + { + inner = new ArrayDictionary(Cleaned(items)); } [Pure] public Roles Remove(string name) { - return new Roles(Without(name)); + return new Roles(inner.Without(name)); } [Pure] @@ -38,12 +90,12 @@ namespace Squidex.Domain.Apps.Core.Apps { var newRole = new Role(name); - if (ContainsKey(name)) + if (inner.ContainsKey(name)) { throw new ArgumentException("Name already exists.", nameof(name)); } - return new Roles(With(name, newRole)); + return new Roles(inner.With(name, newRole)); } [Pure] @@ -52,24 +104,71 @@ namespace Squidex.Domain.Apps.Core.Apps Guard.NotNullOrEmpty(name, nameof(name)); Guard.NotNull(permissions, nameof(permissions)); - if (!TryGetValue(name, out var role)) + if (!inner.TryGetValue(name, out var role)) { return this; } - return new Roles(With(name, role.Update(permissions))); + return new Roles(inner.With(name, role.Update(permissions))); + } + + public static bool IsDefault(string role) + { + return role != null && Defaults.ContainsKey(role); + } + + public static bool IsDefault(Role role) + { + return role != null && Defaults.ContainsKey(role.Name); + } + + public bool ContainsCustom(string name) + { + return inner.ContainsKey(name); + } + + public bool Contains(string name) + { + return inner.ContainsKey(name) || Defaults.ContainsKey(name); + } + + public bool TryGet(string app, string name, out Role value) + { + Guard.NotNull(app, nameof(app)); + + value = null; + + if (Defaults.TryGetValue(name, out var role) || inner.TryGetValue(name, out role)) + { + value = role.ForApp(app); + return true; + } + + return false; + } + + private static string Clean(string permission) + { + permission = Permissions.ForApp(permission).Id; + + var prefix = Permissions.ForApp(Permissions.App); + + if (permission.StartsWith(prefix.Id, StringComparison.OrdinalIgnoreCase)) + { + permission = permission.Substring(prefix.Id.Length); + } + + if (permission.Length == 0) + { + return Permission.Any; + } + + return permission.Substring(1); } - public static Roles CreateDefaults(string app) + private static KeyValuePair[] Cleaned(IEnumerable> items) { - return new Roles( - new Dictionary - { - [Role.Developer] = Role.CreateDeveloper(app), - [Role.Editor] = Role.CreateEditor(app), - [Role.Owner] = Role.CreateOwner(app), - [Role.Reader] = Role.CreateReader(app) - }.ToArray()); + return items.Where(x => !Defaults.ContainsKey(x.Key)).ToArray(); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs index 8b6c1b4a0..3334518c5 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs @@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards e(Not.DefinedOr("name", "role"), nameof(command.Name), nameof(command.Role)); } - if (command.Role != null && !roles.ContainsKey(command.Role)) + if (command.Role != null && !roles.Contains(command.Role)) { e(Not.Valid("role"), nameof(command.Role)); } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs index 56d0d7888..120b0d44d 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards return Validate.It(() => "Cannot assign contributor.", async e => { - if (!roles.ContainsKey(command.Role)) + if (!roles.Contains(command.Role)) { e(Not.Valid("role"), nameof(command.Role)); } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs index bec80a149..bd75bc92e 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { e(Not.Defined("Name"), nameof(command.Name)); } - else if (roles.ContainsKey(command.Name)) + else if (roles.Contains(command.Name)) { e("A role with the same name already exists."); } @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { Guard.NotNull(command, nameof(command)); - GetRoleOrThrow(roles, command.Name); + CheckRoleExists(roles, command.Name); Validate.It(() => "Cannot delete role.", e => { @@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { e(Not.Defined("Name"), nameof(command.Name)); } - else if (Role.IsDefaultRole(command.Name)) + else if (Roles.IsDefault(command.Name)) { e("Cannot delete a default role."); } @@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { Guard.NotNull(command, nameof(command)); - GetRoleOrThrow(roles, command.Name); + CheckRoleExists(roles, command.Name); Validate.It(() => "Cannot delete role.", e => { @@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { e(Not.Defined("Name"), nameof(command.Name)); } - else if (Role.IsDefaultRole(command.Name)) + else if (Roles.IsDefault(command.Name)) { e("Cannot update a default role."); } @@ -86,19 +86,17 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards }); } - private static Role GetRoleOrThrow(Roles roles, string name) + private static void CheckRoleExists(Roles roles, string name) { - if (string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(name) || Roles.IsDefault(name)) { - return null; + return; } - if (!roles.TryGetValue(name, out var role)) + if (!roles.ContainsCustom(name)) { throw new DomainObjectNotFoundException(name, "Roles", typeof(IAppEntity)); } - - return role; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs index d263255aa..9ec0f9144 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs @@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { Guard.NotNull(command, nameof(command)); - GetWorkflowOrThrow(workflows, command.WorkflowId); + CheckWorkflowExists(workflows, command.WorkflowId); Validate.It(() => "Cannot update workflow.", e => { @@ -94,17 +94,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { Guard.NotNull(command, nameof(command)); - GetWorkflowOrThrow(workflows, command.WorkflowId); + CheckWorkflowExists(workflows, command.WorkflowId); } - private static Workflow GetWorkflowOrThrow(Workflows workflows, Guid id) + private static void CheckWorkflowExists(Workflows workflows, Guid id) { - if (!workflows.TryGetValue(id, out var workflow)) + if (!workflows.ContainsKey(id)) { throw new DomainObjectNotFoundException(id.ToString(), "Workflows", typeof(IAppEntity)); } - - return workflow; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs b/src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs deleted file mode 100644 index f6464d5dc..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Squidex.Infrastructure.Security; -using Squidex.Shared; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public static class RoleExtensions - { - public static string[] Prefix(this string[] permissions, string name) - { - var result = new string[permissions.Length + 1]; - - result[0] = Permissions.ForApp(Permissions.AppCommon, name).Id; - - if (permissions.Length > 0) - { - var prefix = Permissions.ForApp(Permissions.App, name).Id; - - for (var i = 0; i < permissions.Length; i++) - { - result[i + 1] = string.Concat(prefix, ".", permissions[i]); - } - } - - permissions = result.Distinct().ToArray(); - - return permissions; - } - - public static PermissionSet WithoutApp(this PermissionSet set, string name) - { - var prefix = Permissions.ForApp(Permissions.App, name).Id; - - return new PermissionSet(set.Select(x => - { - var id = x.Id; - - if (string.Equals(id, prefix, StringComparison.OrdinalIgnoreCase)) - { - return Permission.Any; - } - else if (id.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return id.Substring(prefix.Length + 1); - } - else - { - return id; - } - }).Where(x => x != "common")); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs index 07891e285..a5db89f6f 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs @@ -12,6 +12,8 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Security; using Squidex.Shared; +#pragma warning disable IDE0028 // Simplify collection initialization + namespace Squidex.Domain.Apps.Entities.Apps { public sealed class RolePermissionsProvider diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 673e87e76..5247bb3dc 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -62,8 +62,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.State { case AppCreated e: { - Roles = Roles.CreateDefaults(e.Name); - SimpleMapper.Map(e, this); break; @@ -204,7 +202,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State case AppRoleUpdated e: { - Roles = Roles.Update(e.Name, e.Permissions.Prefix(Name)); + Roles = Roles.Update(e.Name, e.Permissions); break; } diff --git a/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs b/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs index aa4e5796d..90d547bfa 100644 --- a/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs +++ b/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs @@ -132,6 +132,8 @@ namespace Squidex.Infrastructure.Collections public bool TryGetValue(TKey key, out TValue value) { + value = default; + for (var i = 0; i < items.Length; i++) { if (keyComparer.Equals(items[i].Key, key)) @@ -141,7 +143,6 @@ namespace Squidex.Infrastructure.Collections } } - value = default; return false; } diff --git a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index e82fabe02..70091fd1c 100644 --- a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -185,7 +185,7 @@ namespace Squidex.Infrastructure.UsageTracking private static string GetCategory(string category) { - return !string.IsNullOrWhiteSpace(category) ? category.Trim() : "*"; + return !string.IsNullOrWhiteSpace(category) ? category.Trim() : FallbackCategory; } private static string GetKey(string key) diff --git a/src/Squidex.Shared/Permissions.cs b/src/Squidex.Shared/Permissions.cs index 66e631ea2..1d7e6bdd0 100644 --- a/src/Squidex.Shared/Permissions.cs +++ b/src/Squidex.Shared/Permissions.cs @@ -153,11 +153,11 @@ namespace Squidex.Shared } } - public static Permission ForApp(string id, string app = "*", string schema = "*") + public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any) { Guard.NotNull(id, nameof(id)); - return new Permission(id.Replace("{app}", app ?? "*").Replace("{name}", schema ?? "*")); + return new Permission(id.Replace("{app}", app ?? Permission.Any).Replace("{name}", schema ?? Permission.Any)); } public static PermissionSet ToAppPermissions(this PermissionSet permissions, string app) diff --git a/src/Squidex.Web/PermissionExtensions.cs b/src/Squidex.Web/PermissionExtensions.cs index b82b2ade3..68a2df38a 100644 --- a/src/Squidex.Web/PermissionExtensions.cs +++ b/src/Squidex.Web/PermissionExtensions.cs @@ -38,9 +38,9 @@ namespace Squidex.Web return controller.HttpContext.HasPermission(permission) || additional?.Allows(permission) == true; } - public static bool HasPermission(this ApiController controller, string id, string app = "*", string schema = "*", PermissionSet additional = null) + public static bool HasPermission(this ApiController controller, string id, string app = Permission.Any, string schema = Permission.Any, PermissionSet additional = null) { - if (app == "*") + if (app == Permission.Any) { if (controller.RouteData.Values.TryGetValue("app", out var value) && value is string s) { @@ -48,7 +48,7 @@ namespace Squidex.Web } } - if (schema == "*") + if (schema == Permission.Any) { if (controller.RouteData.Values.TryGetValue("name", out var value) && value is string s) { diff --git a/src/Squidex.Web/Pipeline/AppResolver.cs b/src/Squidex.Web/Pipeline/AppResolver.cs index 1ef527d03..33782219b 100644 --- a/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/src/Squidex.Web/Pipeline/AppResolver.cs @@ -90,7 +90,7 @@ namespace Squidex.Web.Pipeline { var clientId = user.GetClientId(); - if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGetValue(client.Role, out var role)) + if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGet(app.Name, client.Role, out var role)) { return (client.Role, role.Permissions); } @@ -102,7 +102,7 @@ namespace Squidex.Web.Pipeline { var subjectId = user.OpenIdSubject(); - if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGetValue(roleName, out var role)) + if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) { return (roleName, role.Permissions); } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index 252224238..526b9c631 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -104,7 +104,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models result.CanAccessApi = true; } - if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppContents, app.Name, "*"), permissions)) + if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppContents, app.Name), permissions)) { result.CanAccessContent = true; } @@ -119,7 +119,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models { var permissions = new List(); - if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGetValue(roleName, out var role)) + if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) { permissions.AddRange(role.Permissions); } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs index 1617c69a6..2faadfb53 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs @@ -46,7 +46,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public static RoleDto FromRole(Role role, IAppEntity app) { - var permissions = role.Permissions.WithoutApp(app.Name); + var permissions = role.Permissions; var result = new RoleDto { @@ -54,7 +54,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models NumClients = GetNumClients(role, app), NumContributors = GetNumContributors(role, app), Permissions = permissions.ToIds(), - IsDefaultRole = Role.IsDefaultRole(role.Name) + IsDefaultRole = role.IsDefault }; return result; @@ -62,12 +62,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models private static int GetNumContributors(Role role, IAppEntity app) { - return app.Contributors.Count(x => Role.IsRole(x.Value, role.Name)); + return app.Contributors.Count(x => role.Equals(x.Value)); } private static int GetNumClients(Role role, IAppEntity app) { - return app.Clients.Count(x => Role.IsRole(x.Value.Role, role.Name)); + return app.Clients.Count(x => role.Equals(x.Value.Role)); } public RoleDto WithLinks(ApiController controller, string app) diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs index c7daa77df..a2a6cb3b2 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs @@ -28,7 +28,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models var result = new RolesDto { Items = - app.Roles.Values + app.Roles.All .Select(x => RoleDto.FromRole(x, app)) .Select(x => x.WithLinks(controller, appName)) .OrderBy(x => x.Name) diff --git a/src/Squidex/app/app.routes.ts b/src/Squidex/app/app.routes.ts index 8331b68a4..8620adf07 100644 --- a/src/Squidex/app/app.routes.ts +++ b/src/Squidex/app/app.routes.ts @@ -10,6 +10,7 @@ import { RouterModule, Routes } from '@angular/router'; import { AppAreaComponent, + ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LoginPageComponent, @@ -91,6 +92,10 @@ export const routes: Routes = [ path: 'login', component: LoginPageComponent }, + { + path: 'forbidden', + component: ForbiddenPageComponent + }, { path: '**', component: NotFoundPageComponent diff --git a/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts b/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts index a7206035c..5f285d96f 100644 --- a/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts +++ b/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts @@ -8,6 +8,7 @@ import { HTTP_INTERCEPTORS, HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; import { of } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, Mock, Times } from 'typemoq'; @@ -17,15 +18,19 @@ import { AuthInterceptor } from './auth.interceptor'; describe('AuthInterceptor', () => { let authService: IMock; + let router: IMock; beforeEach(() => { authService = Mock.ofType(AuthService); + router = Mock.ofType(); + TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ], providers: [ + { provide: Router, useFactory: () => router.object }, { provide: AuthService, useValue: authService.object }, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { @@ -103,7 +108,7 @@ describe('AuthInterceptor', () => { })); [403].forEach(statusCode => { - it(`should logout for ${statusCode} status code`, + it(`should redirect for ${statusCode} status code`, inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => { authService.setup(x => x.userChanges).returns(() => of({ authToken: 'letmein' })); @@ -116,7 +121,7 @@ describe('AuthInterceptor', () => { expect().nothing(); - authService.verify(x => x.logoutRedirect(), Times.once()); + router.verify(x => x.navigate(['/forbidden'], { replaceUrl: true }), Times.once()); })); }); diff --git a/src/Squidex/app/shared/interceptors/auth.interceptor.ts b/src/Squidex/app/shared/interceptors/auth.interceptor.ts index c9f92ff09..026010907 100644 --- a/src/Squidex/app/shared/interceptors/auth.interceptor.ts +++ b/src/Squidex/app/shared/interceptors/auth.interceptor.ts @@ -7,6 +7,7 @@ import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Injectable} from '@angular/core'; +import { Router } from '@angular/router'; import { empty, Observable, throwError } from 'rxjs'; import { catchError, switchMap, take } from 'rxjs/operators'; @@ -19,7 +20,8 @@ export class AuthInterceptor implements HttpInterceptor { private baseUrl: string; constructor(apiUrlConfig: ApiUrlConfig, - private readonly authService: AuthService + private readonly authService: AuthService, + private readonly router: Router ) { this.baseUrl = apiUrlConfig.buildUrl(''); } @@ -58,7 +60,11 @@ export class AuthInterceptor implements HttpInterceptor { switchMap(u => this.makeRequest(req, next, u))); } else if (error.status === 401 || error.status === 403) { if (req.method === 'GET') { - this.authService.logoutRedirect(); + if (error.status === 401) { + this.authService.logoutRedirect(); + } else { + this.router.navigate(['/forbidden'], { replaceUrl: true }); + } return empty(); } else { diff --git a/src/Squidex/app/shell/declarations.ts b/src/Squidex/app/shell/declarations.ts index 13540c252..0224944b6 100644 --- a/src/Squidex/app/shell/declarations.ts +++ b/src/Squidex/app/shell/declarations.ts @@ -7,14 +7,11 @@ export * from './pages/app/app-area.component'; export * from './pages/app/left-menu.component'; - +export * from './pages/forbidden/forbidden-page.component'; export * from './pages/home/home-page.component'; - export * from './pages/internal/apps-menu.component'; export * from './pages/internal/internal-area.component'; export * from './pages/internal/profile-menu.component'; - export * from './pages/login/login-page.component'; export * from './pages/logout/logout-page.component'; - export * from './pages/not-found/not-found-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/shell/module.ts b/src/Squidex/app/shell/module.ts index 7fa58f5de..a3ccf1762 100644 --- a/src/Squidex/app/shell/module.ts +++ b/src/Squidex/app/shell/module.ts @@ -12,6 +12,7 @@ import { SqxFrameworkModule, SqxSharedModule } from '@app/shared'; import { AppAreaComponent, AppsMenuComponent, + ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LeftMenuComponent, @@ -29,12 +30,14 @@ import { exports: [ AppAreaComponent, HomePageComponent, + ForbiddenPageComponent, InternalAreaComponent, NotFoundPageComponent ], declarations: [ AppAreaComponent, AppsMenuComponent, + ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LeftMenuComponent, diff --git a/src/Squidex/app/shell/pages/forbidden/forbidden-page.component.ts b/src/Squidex/app/shell/pages/forbidden/forbidden-page.component.ts new file mode 100644 index 000000000..ea774b467 --- /dev/null +++ b/src/Squidex/app/shell/pages/forbidden/forbidden-page.component.ts @@ -0,0 +1,32 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Location } from '@angular/common'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'sqx-forbidden-page', + template: ` + + + + ` +}) +export class ForbiddenPageComponent { + constructor( + private readonly location: Location + ) { + } + + public back() { + this.location.back(); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/not-found/not-found-page.component.html b/src/Squidex/app/shell/pages/not-found/not-found-page.component.html deleted file mode 100644 index eb372c93c..000000000 --- a/src/Squidex/app/shell/pages/not-found/not-found-page.component.html +++ /dev/null @@ -1,13 +0,0 @@ - - -
- - -

Not Found

- -

- Sorry, the page or resource you are looking for does not exist. -

- - Back to previous page. -
\ No newline at end of file diff --git a/src/Squidex/app/shell/pages/not-found/not-found-page.component.scss b/src/Squidex/app/shell/pages/not-found/not-found-page.component.scss deleted file mode 100644 index d1951ab29..000000000 --- a/src/Squidex/app/shell/pages/not-found/not-found-page.component.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '_mixins'; -@import '_vars'; \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/not-found/not-found-page.component.ts b/src/Squidex/app/shell/pages/not-found/not-found-page.component.ts index 4df1b1a22..55748b9e4 100644 --- a/src/Squidex/app/shell/pages/not-found/not-found-page.component.ts +++ b/src/Squidex/app/shell/pages/not-found/not-found-page.component.ts @@ -10,8 +10,15 @@ import { Component } from '@angular/core'; @Component({ selector: 'sqx-not-found-page', - styleUrls: ['./not-found-page.component.scss'], - templateUrl: './not-found-page.component.html' + template: ` + + + + ` }) export class NotFoundPageComponent { constructor( diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs new file mode 100644 index 000000000..b96f6637d --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Apps +{ + public class RoleTests + { + [Fact] + public void Should_be_default_role() + { + var role = new Role("Owner"); + + Assert.True(role.IsDefault); + } + + [Fact] + public void Should_not_be_default_role() + { + var role = new Role("Custom"); + + Assert.False(role.IsDefault); + } + + [Fact] + public void Should_add_common_permission() + { + var role = new Role("Name"); + + var result = role.ForApp("my-app").Permissions.ToIds(); + + Assert.Equal(new[] { "squidex.apps.my-app.common" }, result); + } + + [Fact] + public void Should_not_have_duplicate_permission() + { + var role = new Role("Name", "common", "common", "common"); + + var result = role.ForApp("my-app").Permissions.ToIds(); + + Assert.Single(result); + } + + [Fact] + public void Should_ForApp_permission() + { + var role = new Role("Name", "clients.read"); + + var result = role.ForApp("my-app").Permissions.ToIds(); + + Assert.Equal("squidex.apps.my-app.clients.read", result.ElementAt(1)); + } + + [Fact] + public void Should_check_for_name() + { + var role = new Role("Custom"); + + Assert.True(role.Equals("Custom")); + } + + [Fact] + public void Should_check_for_null_name() + { + var role = new Role("Custom"); + + Assert.False(role.Equals(null)); + Assert.False(role.Equals("Other")); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs index d79477f0a..5d143f13c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs @@ -16,11 +16,21 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_serialize_and_deserialize() { - var sut = Roles.CreateDefaults("my-app"); + var sut = Roles.Empty.Add("Custom").Update("Custom", "Permission1", "Permission2"); var roles = sut.SerializeAndDeserialize(); roles.Should().BeEquivalentTo(sut); } + + [Fact] + public void Should_serialize_and_deserialize_empty() + { + var sut = Roles.Empty; + + var roles = sut.SerializeAndDeserialize(); + + Assert.Same(Roles.Empty, roles); + } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index 591708388..d669af3ea 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Linq; using FluentAssertions; using Squidex.Domain.Apps.Core.Apps; using Squidex.Infrastructure.Security; @@ -26,6 +27,14 @@ namespace Squidex.Domain.Apps.Core.Model.Apps roles_0 = Roles.Empty.Add(firstRole); } + [Fact] + public void Should_create_roles_without_defaults() + { + var roles = new Roles(Roles.Defaults.ToArray()); + + Assert.Equal(0, roles.CustomCount); + } + [Fact] public void Should_add_role() { @@ -63,7 +72,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var roles_1 = roles_0.Remove(firstRole); - Assert.Empty(roles_1); + Assert.Equal(0, roles_1.CustomCount); } [Fact] @@ -71,23 +80,75 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var roles_1 = roles_0.Remove(role); - Assert.NotEmpty(roles_1); + Assert.True(roles_1.CustomCount > 0); + } + + [Fact] + public void Should_get_custom_roles() + { + var names = roles_0.Custom.Select(x => x.Name).ToArray(); + + Assert.Equal(new[] { firstRole }, names); } [Fact] - public void Should_create_defaults() + public void Should_get_all_roles() { - var sut = Roles.CreateDefaults("my-app"); + var names = roles_0.All.Select(x => x.Name).ToArray(); - Assert.Equal(4, sut.Count); + Assert.Equal(new[] { firstRole, "Owner", "Reader", "Editor", "Developer" }, names); + } - foreach (var sutRole in sut) + [Fact] + public void Should_check_for_custom_role() + { + Assert.True(roles_0.ContainsCustom(firstRole)); + } + + [Fact] + public void Should_check_for_non_custom_role() + { + Assert.False(roles_0.ContainsCustom(Role.Owner)); + } + + [Fact] + public void Should_check_for_default_role() + { + Assert.True(Roles.IsDefault(Role.Owner)); + } + + [Fact] + public void Should_check_for_non_default_role() + { + Assert.False(Roles.IsDefault(firstRole)); + } + + [InlineData("Developer")] + [InlineData("Editor")] + [InlineData("Owner")] + [InlineData("Reader")] + [Theory] + public void Should_get_default_roles(string name) + { + var found = roles_0.TryGet("app", name, out var role); + + Assert.True(found); + Assert.True(role.IsDefault); + Assert.True(roles_0.Contains(name)); + + foreach (var permission in role.Permissions) { - foreach (var permission in sutRole.Value.Permissions) - { - Assert.StartsWith("squidex.apps.my-app", permission.Id); - } + Assert.StartsWith("squidex.apps.app.", permission.Id); } } + + [Fact] + public void Should_return_null_if_role_not_found() + { + var found = roles_0.TryGet("app", "custom", out var role); + + Assert.False(found); + Assert.Null(role); + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs index 2d3bd0f6b..36d8ef06a 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs @@ -487,7 +487,7 @@ namespace Squidex.Domain.Apps.Entities.Apps result.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(5, sut.Snapshot.Roles.Count); + Assert.Equal(1, sut.Snapshot.Roles.CustomCount); LastEvents .ShouldHaveSameEvents( @@ -507,7 +507,7 @@ namespace Squidex.Domain.Apps.Entities.Apps result.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(4, sut.Snapshot.Roles.Count); + Assert.Equal(0, sut.Snapshot.Roles.CustomCount); LastEvents .ShouldHaveSameEvents( diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs index f2d30ffd8..6a27610ce 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards public class GuardAppClientsTests { private readonly AppClients clients_0 = AppClients.Empty; - private readonly Roles roles = Roles.CreateDefaults("my-app"); + private readonly Roles roles = Roles.Empty; [Fact] public void CanAttach_should_throw_execption_if_client_id_is_null() diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs index 94a14c1f3..da4322d27 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards private readonly IUserResolver users = A.Fake(); private readonly IAppLimitsPlan appPlan = A.Fake(); private readonly AppContributors contributors_0 = AppContributors.Empty; - private readonly Roles roles = Roles.CreateDefaults("my-app"); + private readonly Roles roles = Roles.Empty; public GuardAppContributorsTests() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RoleExtensionsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RoleExtensionsTests.cs deleted file mode 100644 index cfd3c06af..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RoleExtensionsTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Squidex.Infrastructure.Security; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class RoleExtensionsTests - { - [Fact] - public void Should_add_common_permission() - { - var source = Array.Empty(); - var result = source.Prefix("my-app"); - - Assert.Equal(new[] { "squidex.apps.my-app.common" }, result); - } - - [Fact] - public void Should_not_have_duplicate_permission() - { - var source = new[] { "common", "common", "common" }; - var result = source.Prefix("my-app"); - - Assert.Single(result); - } - - [Fact] - public void Should_prefix_permission() - { - var source = new[] { "clients.read" }; - var result = source.Prefix("my-app"); - - Assert.Equal("squidex.apps.my-app.clients.read", result[1]); - } - - [Fact] - public void Should_remove_app_prefix() - { - var source = new PermissionSet("squidex.apps.my-app.clients"); - var result = source.WithoutApp("my-app"); - - Assert.Equal("clients", result.First().Id); - } - - [Fact] - public void Should_not_remove_app_prefix_when_other_app() - { - var source = new PermissionSet("squidex.apps.other-app.clients"); - var result = source.WithoutApp("my-app"); - - Assert.Equal("squidex.apps.other-app.clients", result.First().Id); - } - - [Fact] - public void Should_set_to_wildcard_when_app_root_permission() - { - var source = new PermissionSet("squidex.apps.my-app"); - var result = source.WithoutApp("my-app"); - - Assert.Equal(Permission.Any, result.First().Id); - } - - [Fact] - public void Should_remove_common_permission() - { - var source = new PermissionSet("squidex.apps.my-app.common"); - var result = source.WithoutApp("my-app"); - - Assert.Empty(result); - } - } -} diff --git a/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs index cf2ff94ec..a27aebb0a 100644 --- a/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ b/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -185,8 +185,11 @@ namespace Squidex.Web.Pipeline .Returns(AppClients.Empty); } + A.CallTo(() => appEntity.Name) + .Returns(name); + A.CallTo(() => appEntity.Roles) - .Returns(Roles.CreateDefaults(name)); + .Returns(Roles.Empty); return appEntity; }