Browse Source

Not allowed page. (#430)

* Not allowed page.
* Do not restore default roles in DB.
pull/432/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
94a5fcd25f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs
  2. 13
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs
  3. 63
      src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  4. 135
      src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  5. 2
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
  6. 2
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  7. 20
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs
  8. 10
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs
  9. 61
      src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs
  10. 2
      src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs
  11. 4
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  12. 3
      src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs
  13. 2
      src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs
  14. 4
      src/Squidex.Shared/Permissions.cs
  15. 6
      src/Squidex.Web/PermissionExtensions.cs
  16. 4
      src/Squidex.Web/Pipeline/AppResolver.cs
  17. 4
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  18. 8
      src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs
  19. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs
  20. 5
      src/Squidex/app/app.routes.ts
  21. 9
      src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts
  22. 8
      src/Squidex/app/shared/interceptors/auth.interceptor.ts
  23. 5
      src/Squidex/app/shell/declarations.ts
  24. 3
      src/Squidex/app/shell/module.ts
  25. 32
      src/Squidex/app/shell/pages/forbidden/forbidden-page.component.ts
  26. 13
      src/Squidex/app/shell/pages/not-found/not-found-page.component.html
  27. 2
      src/Squidex/app/shell/pages/not-found/not-found-page.component.scss
  28. 11
      src/Squidex/app/shell/pages/not-found/not-found-page.component.ts
  29. 79
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs
  30. 12
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs
  31. 77
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs
  32. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs
  33. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs
  34. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs
  35. 80
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/RoleExtensionsTests.cs
  36. 5
      tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs

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

13
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<string, string[]>(value.Count);
var json = new Dictionary<string, string[]>(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<Dictionary<string, string[]>>(reader);
return new Roles(json.Select(Convert).ToArray());
if (json.Count == 0)
{
return Roles.Empty;
}
return new Roles(json.Select(Convert));
}
private static KeyValuePair<string, Role> Convert(KeyValuePair<string, string[]> kvp)

63
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<string> DefaultRolesSet = new HashSet<string>(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)
public bool Equals(string name)
{
return role != null && DefaultRolesSet.Contains(role);
return name != null && name.Equals(Name, StringComparison.Ordinal);
}
public static bool IsRole(string name, string expected)
public Role ForApp(string app)
{
return name != null && string.Equals(name, expected, StringComparison.OrdinalIgnoreCase);
}
public static Role CreateOwner(string app)
var result = new HashSet<Permission>
{
return new Role(Owner,
P.ForApp(P.App, app));
}
AllPermissions.ForApp(AllPermissions.AppCommon, app)
};
public static Role CreateEditor(string app)
if (Permissions.Any())
{
return new Role(Editor,
P.ForApp(P.AppAssets, app),
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContents, app),
P.ForApp(P.AppWorkflowsRead, app));
}
var prefix = AllPermissions.ForApp(AllPermissions.App, app).Id;
public static Role CreateReader(string app)
foreach (var permission in Permissions)
{
return new Role(Reader,
P.ForApp(P.AppAssetsRead, app),
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContentsRead, app));
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));
}
}
}

135
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<string, Role>
public sealed class Roles
{
public static readonly Roles Empty = new Roles();
private readonly ArrayDictionary<string, Role> inner;
private Roles()
public static readonly IReadOnlyDictionary<string, Role> Defaults = new Dictionary<string, Role>
{
[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<string, Role>());
public int CustomCount
{
get { return inner.Count; }
}
public Role this[string name]
{
get { return inner[name]; }
}
public IEnumerable<Role> Custom
{
get { return inner.Values; }
}
public IEnumerable<Role> All
{
get { return inner.Values.Union(Defaults.Values); }
}
public Roles(KeyValuePair<string, Role>[] items)
: base(items)
private Roles(ArrayDictionary<string, Role> roles)
{
inner = roles;
}
public Roles(IEnumerable<KeyValuePair<string, Role>> items)
{
inner = new ArrayDictionary<string, Role>(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 Roles CreateDefaults(string app)
public static bool IsDefault(Role role)
{
return new Roles(
new Dictionary<string, 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);
}
private static KeyValuePair<string, Role>[] Cleaned(IEnumerable<KeyValuePair<string, Role>> items)
{
[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();
}
}
}

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

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

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

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

61
src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs

@ -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"));
}
}
}

2
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

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

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

2
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)

4
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)

6
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)
{

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

4
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<Permission>();
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);
}

8
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)

2
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)

5
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

9
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<AuthService>;
let router: IMock<Router>;
beforeEach(() => {
authService = Mock.ofType(AuthService);
router = Mock.ofType<Router>();
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(<any>{ 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());
}));
});

8
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') {
if (error.status === 401) {
this.authService.logoutRedirect();
} else {
this.router.navigate(['/forbidden'], { replaceUrl: true });
}
return empty();
} else {

5
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';

3
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,

32
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: `
<sqx-title message="Not Found"></sqx-title>
<div class="landing-page">
<img class="splash-image" src="~/../squid.svg?title=FORBIDDEN&text=You%20are%20not%20allowed%20to%20view%20this%20page&background=white&small" />
<a href="#" (click)="back()">Back to previous page.</a>
</div>
`
})
export class ForbiddenPageComponent {
constructor(
private readonly location: Location
) {
}
public back() {
this.location.back();
}
}

13
src/Squidex/app/shell/pages/not-found/not-found-page.component.html

@ -1,13 +0,0 @@
<sqx-title message="Not Found"></sqx-title>
<div class="landing-page">
<img class="splash-image" src="~/../squid.svg?title=OH%20DAMN&text=This%20is%20not%20the%20page%20you%20are%20looking%20for!&background=white&small" />
<h1>Not Found</h1>
<p>
Sorry, the page or resource you are looking for does not exist.
</p>
<a href="#" (click)="back()">Back to previous page.</a>
</div>

2
src/Squidex/app/shell/pages/not-found/not-found-page.component.scss

@ -1,2 +0,0 @@
@import '_mixins';
@import '_vars';

11
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: `
<sqx-title message="Not Found"></sqx-title>
<div class="landing-page">
<img class="splash-image" src="~/../squid.svg?title=Not Found&text=This%20is%20not%20the%20page%20you%20are%20looking%20for!&background=white&small" />
<a href="#" (click)="back()">Back to previous page.</a>
</div>
`
})
export class NotFoundPageComponent {
constructor(

79
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"));
}
}
}

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

77
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_create_defaults()
public void Should_get_custom_roles()
{
var sut = Roles.CreateDefaults("my-app");
var names = roles_0.Custom.Select(x => x.Name).ToArray();
Assert.Equal(4, sut.Count);
Assert.Equal(new[] { firstRole }, names);
}
foreach (var sutRole in sut)
[Fact]
public void Should_get_all_roles()
{
foreach (var permission in sutRole.Value.Permissions)
var names = roles_0.All.Select(x => x.Name).ToArray();
Assert.Equal(new[] { firstRole, "Owner", "Reader", "Editor", "Developer" }, names);
}
[Fact]
public void Should_check_for_custom_role()
{
Assert.StartsWith("squidex.apps.my-app", permission.Id);
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)
{
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);
}
}
}

4
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(

2
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()

2
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<IUserResolver>();
private readonly IAppLimitsPlan appPlan = A.Fake<IAppLimitsPlan>();
private readonly AppContributors contributors_0 = AppContributors.Empty;
private readonly Roles roles = Roles.CreateDefaults("my-app");
private readonly Roles roles = Roles.Empty;
public GuardAppContributorsTests()
{

80
tests/Squidex.Domain.Apps.Entities.Tests/Apps/RoleExtensionsTests.cs

@ -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<string>();
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);
}
}
}

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

Loading…
Cancel
Save