Browse Source

Feature/api permissions (#518)

* Simplify api permissions and improve performance with simple caching.

* Schema resolver tests.

* IDentity server improvements.

* Set base url.

* Code cleanup and tests fixed.
pull/520/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
3d2e78e75c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  2. 4
      backend/src/Squidex.Shared/Permissions.cs
  3. 9
      backend/src/Squidex.Web/ApiController.cs
  4. 22
      backend/src/Squidex.Web/ApiPermissionAttribute.cs
  5. 2
      backend/src/Squidex.Web/Constants.cs
  6. 3
      backend/src/Squidex.Web/FodyWeavers.xml
  7. 26
      backend/src/Squidex.Web/FodyWeavers.xsd
  8. 17
      backend/src/Squidex.Web/ISchemaFeature.cs
  9. 64
      backend/src/Squidex.Web/PermissionExtensions.cs
  10. 19
      backend/src/Squidex.Web/Pipeline/AccessTokenQueryExtensions.cs
  11. 35
      backend/src/Squidex.Web/Pipeline/AccessTokenQueryMiddleware.cs
  12. 22
      backend/src/Squidex.Web/Pipeline/SchemaFeature.cs
  13. 66
      backend/src/Squidex.Web/Pipeline/SchemaResolver.cs
  14. 242
      backend/src/Squidex.Web/Resources.cs
  15. 5
      backend/src/Squidex.Web/Squidex.Web.csproj
  16. 36
      backend/src/Squidex/Areas/Api/Config/IdentityServerPathMiddleware.cs
  17. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  18. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  19. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  20. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  21. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
  22. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs
  23. 13
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  24. 102
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  25. 11
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs
  26. 17
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs
  27. 11
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs
  28. 15
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs
  29. 15
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs
  30. 15
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs
  31. 17
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs
  32. 17
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs
  33. 13
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs
  34. 19
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs
  35. 13
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs
  36. 19
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs
  37. 3
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  38. 4
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs
  39. 10
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  40. 32
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  41. 21
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs
  42. 17
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs
  43. 19
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs
  44. 2
      backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
  45. 15
      backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs
  46. 17
      backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs
  47. 32
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  48. 37
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  49. 19
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  50. 8
      backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs
  51. 15
      backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs
  52. 10
      backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs
  53. 34
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs
  54. 23
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs
  55. 4
      backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  56. 29
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs
  57. 21
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs
  58. 14
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs
  59. 51
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs
  60. 17
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs
  61. 42
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  62. 6
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  63. 21
      backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs
  64. 29
      backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs
  65. 15
      backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs
  66. 12
      backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  67. 6
      backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  68. 3
      backend/src/Squidex/Areas/Api/Startup.cs
  69. 32
      backend/src/Squidex/Config/Authentication/IdentityServerServices.cs
  70. 2
      backend/src/Squidex/Config/MyIdentityOptions.cs
  71. 4
      backend/src/Squidex/Config/Web/WebServices.cs
  72. 6
      backend/src/Squidex/appsettings.json
  73. 29
      backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs
  74. 151
      backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs

6
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -11,7 +11,7 @@ using System.Diagnostics.Contracts;
using System.Linq; using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using AllPermissions = Squidex.Shared.Permissions; using P = Squidex.Shared.Permissions;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
@ -59,12 +59,12 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
var result = new HashSet<Permission> var result = new HashSet<Permission>
{ {
AllPermissions.ForApp(AllPermissions.AppCommon, app) P.ForApp(P.AppCommon, app)
}; };
if (Permissions.Any()) if (Permissions.Any())
{ {
var prefix = AllPermissions.ForApp(AllPermissions.App, app).Id; var prefix = P.ForApp(P.App, app).Id;
foreach (var permission in Permissions) foreach (var permission in Permissions)
{ {

4
backend/src/Squidex.Shared/Permissions.cs

@ -114,8 +114,8 @@ namespace Squidex.Shared
public const string AppRulesDisable = "squidex.apps.{app}.rules.disable"; public const string AppRulesDisable = "squidex.apps.{app}.rules.disable";
public const string AppRulesDelete = "squidex.apps.{app}.rules.delete"; public const string AppRulesDelete = "squidex.apps.{app}.rules.delete";
public const string AppSchemas = "squidex.apps.{app}.schemas.{name}"; public const string AppSchemas = "squidex.apps.{app}.schemas";
public const string AppSchemasCreate = "squidex.apps.{app}.schemas.{name}.create"; public const string AppSchemasCreate = "squidex.apps.{app}.schemas.create";
public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update"; public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update";
public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts"; public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts";
public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{name}.publish"; public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{name}.publish";

9
backend/src/Squidex.Web/ApiController.cs

@ -21,6 +21,8 @@ namespace Squidex.Web
[ApiModelValidation(false)] [ApiModelValidation(false)]
public abstract class ApiController : Controller public abstract class ApiController : Controller
{ {
private readonly Lazy<Resources> resources;
protected ICommandBus CommandBus { get; } protected ICommandBus CommandBus { get; }
protected IAppEntity App protected IAppEntity App
@ -38,6 +40,11 @@ namespace Squidex.Web
} }
} }
protected Resources Resources
{
get { return resources.Value; }
}
protected Context Context protected Context Context
{ {
get { return HttpContext.Context(); } get { return HttpContext.Context(); }
@ -53,6 +60,8 @@ namespace Squidex.Web
Guard.NotNull(commandBus); Guard.NotNull(commandBus);
CommandBus = commandBus; CommandBus = commandBus;
resources = new Lazy<Resources>(() => new Resources(this));
} }
public override void OnActionExecuting(ActionExecutingContext context) public override void OnActionExecuting(ActionExecutingContext context)

22
backend/src/Squidex.Web/ApiPermissionAttribute.cs

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared;
namespace Squidex.Web namespace Squidex.Web
{ {
@ -25,7 +26,7 @@ namespace Squidex.Web
public ApiPermissionAttribute(params string[] ids) public ApiPermissionAttribute(params string[] ids)
{ {
AuthenticationSchemes = "Bearer"; AuthenticationSchemes = Constants.ApiSecurityScheme;
permissionIds = ids; permissionIds = ids;
} }
@ -40,16 +41,25 @@ namespace Squidex.Web
if (permissions != null) if (permissions != null)
{ {
foreach (var permissionId in permissionIds) foreach (var id in permissionIds)
{ {
var id = permissionId; var app = context.HttpContext.Features.Get<IAppFeature>()?.AppId.Name;
foreach (var (key, value) in context.RouteData.Values) if (string.IsNullOrWhiteSpace(app))
{ {
id = id.Replace($"{{{key}}}", value?.ToString()); app = Permission.Any;
} }
if (permissions.Allows(new Permission(id))) var schema = context.HttpContext.Features.Get<ISchemaFeature>()?.SchemaId.Name;
if (string.IsNullOrWhiteSpace(schema))
{
schema = Permission.Any;
}
var permission = Permissions.ForApp(id, app, schema);
if (permissions.Allows(permission))
{ {
hasPermission = true; hasPermission = true;
break; break;

2
backend/src/Squidex.Web/Constants.cs

@ -18,6 +18,8 @@ namespace Squidex.Web
public static readonly string ApiScope = "squidex-api"; public static readonly string ApiScope = "squidex-api";
public static readonly string ApiSecurityScheme = "identity-server";
public static readonly string OrleansClusterId = "squidex-v2"; public static readonly string OrleansClusterId = "squidex-v2";
public static readonly string OrleansPrefix = "/orleans"; public static readonly string OrleansPrefix = "/orleans";

3
backend/src/Squidex.Web/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Lazy />
</Weavers>

26
backend/src/Squidex.Web/FodyWeavers.xsd

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Lazy" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

17
backend/src/Squidex.Web/ISchemaFeature.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Web
{
public interface ISchemaFeature
{
NamedId<Guid> SchemaId { get; }
}
}

64
backend/src/Squidex.Web/PermissionExtensions.cs

@ -1,64 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure.Security;
using AllPermissions = Squidex.Shared.Permissions;
namespace Squidex.Web
{
public static class PermissionExtensions
{
public static PermissionSet Permissions(this HttpContext httpContext)
{
return httpContext.Context().Permissions;
}
public static bool Includes(this HttpContext httpContext, Permission permission, PermissionSet? additional = null)
{
return httpContext.Permissions().Includes(permission) || additional?.Includes(permission) == true;
}
public static bool Includes(this ApiController controller, Permission permission, PermissionSet? additional = null)
{
return controller.HttpContext.Includes(permission) || additional?.Includes(permission) == true;
}
public static bool HasPermission(this HttpContext httpContext, Permission permission, PermissionSet? additional = null)
{
return httpContext.Permissions().Allows(permission) || additional?.Allows(permission) == true;
}
public static bool HasPermission(this ApiController controller, Permission permission, PermissionSet? additional = null)
{
return controller.HttpContext.HasPermission(permission) || additional?.Allows(permission) == true;
}
public static bool HasPermission(this ApiController controller, string id, string app = Permission.Any, string schema = Permission.Any, PermissionSet? additional = null)
{
if (app == Permission.Any)
{
if (controller.RouteData.Values.TryGetValue("app", out var value) && value is string s)
{
app = s;
}
}
if (schema == Permission.Any)
{
if (controller.RouteData.Values.TryGetValue("name", out var value) && value is string s)
{
schema = s;
}
}
var permission = AllPermissions.ForApp(id, app, schema);
return controller.HasPermission(permission, additional);
}
}
}

19
backend/src/Squidex.Web/Pipeline/AccessTokenQueryExtensions.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Builder;
namespace Squidex.Web.Pipeline
{
public static class AccessTokenQueryExtensions
{
public static void UseAccessTokenQueryString(this IApplicationBuilder app)
{
app.UseMiddleware<AccessTokenQueryMiddleware>();
}
}
}

35
backend/src/Squidex.Web/Pipeline/AccessTokenQueryMiddleware.cs

@ -0,0 +1,35 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Squidex.Web.Pipeline
{
public sealed class AccessTokenQueryMiddleware
{
private readonly RequestDelegate next;
public AccessTokenQueryMiddleware(RequestDelegate next)
{
this.next = next;
}
public Task InvokeAsync(HttpContext context)
{
var request = context.Request;
if (!string.IsNullOrWhiteSpace(request.Headers[HeaderNames.Authorization]) && request.Query.TryGetValue("access_token", out var token))
{
request.Headers[HeaderNames.Authorization] = token;
}
return next(context);
}
}
}

22
backend/src/Squidex.Web/Pipeline/SchemaFeature.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Web.Pipeline
{
public sealed class SchemaFeature : ISchemaFeature
{
public NamedId<Guid> SchemaId { get; }
public SchemaFeature(NamedId<Guid> schemaId)
{
SchemaId = schemaId;
}
}
}

66
backend/src/Squidex.Web/Pipeline/SchemaResolver.cs

@ -0,0 +1,66 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Web.Pipeline
{
public sealed class SchemaResolver : IAsyncActionFilter
{
private readonly IAppProvider appProvider;
public SchemaResolver(IAppProvider appProvider)
{
Guard.NotNull(appProvider);
this.appProvider = appProvider;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var appId = context.HttpContext.Features.Get<IAppFeature>()?.AppId.Id ?? default;
if (appId != default)
{
var schemaIdOrName = context.RouteData.Values["name"]?.ToString();
if (!string.IsNullOrWhiteSpace(schemaIdOrName))
{
var schema = await GetSchemaAsync(appId, schemaIdOrName);
if (schema == null)
{
context.Result = new NotFoundResult();
return;
}
context.HttpContext.Features.Set<ISchemaFeature>(new SchemaFeature(schema.NamedId()));
}
}
await next();
}
private Task<ISchemaEntity?> GetSchemaAsync(Guid appId, string schemaIdOrName)
{
if (Guid.TryParse(schemaIdOrName, out var id))
{
return appProvider.GetSchemaAsync(appId, id);
}
else
{
return appProvider.GetSchemaAsync(appId, schemaIdOrName);
}
}
}
}

242
backend/src/Squidex.Web/Resources.cs

@ -0,0 +1,242 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Lazy;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using P = Squidex.Shared.Permissions;
namespace Squidex.Web
{
public sealed class Resources
{
private readonly Dictionary<(string, string), bool> schemaPermissions = new Dictionary<(string, string), bool>();
// Contents
public bool CanReadContent(string schema) => IsAllowedForSchema(P.AppContentsRead, schema);
public bool CanCreateContent(string schema) => IsAllowedForSchema(P.AppContentsCreate, schema);
public bool CanCreateContentVersion(string schema) => IsAllowedForSchema(P.AppContentsVersionCreate, schema);
public bool CanDeleteContent(string schema) => IsAllowedForSchema(P.AppContentsDelete, schema);
public bool CanDeleteContentVersion(string schema) => IsAllowedForSchema(P.AppContentsVersionDelete, schema);
public bool CanUpdateContent(string schema) => IsAllowedForSchema(P.AppContentsUpdate, schema);
public bool CanUpdateContentPartial(string schema) => IsAllowedForSchema(P.AppContentsUpdatePartial, schema);
// Schemas
public bool CanUpdateSchema(string schema) => IsAllowedForSchema(P.AppSchemasDelete, schema);
public bool CanUpdateSchemaScripts(string schema) => IsAllowedForSchema(P.AppSchemasScripts, schema);
public bool CanPublishSchema(string schema) => IsAllowedForSchema(P.AppSchemasPublish, schema);
public bool CanDeleteSchema(string schema) => IsAllowedForSchema(P.AppSchemasDelete, schema);
[Lazy]
public bool CanCreateSchema => IsAllowed(P.AppSchemasUpdate);
// Contributors
[Lazy]
public bool CanAssignContributor => IsAllowed(P.AppContributorsAssign);
[Lazy]
public bool CanRevokeContributor => IsAllowed(P.AppContributorsRevoke);
// Workflows
[Lazy]
public bool CanCreateWorkflow => IsAllowed(P.AppWorkflowsCreate);
[Lazy]
public bool CanUpdateWorkflow => IsAllowed(P.AppWorkflowsUpdate);
[Lazy]
public bool CanDeleteWorkflow => IsAllowed(P.AppWorkflowsDelete);
// Roles
[Lazy]
public bool CanCreateRole => IsAllowed(P.AppRolesCreate);
[Lazy]
public bool CanUpdateRole => IsAllowed(P.AppRolesUpdate);
[Lazy]
public bool CanDeleteRole => IsAllowed(P.AppRolesDelete);
// Languages
[Lazy]
public bool CanCreateLanguage => IsAllowed(P.AppLanguagesCreate);
[Lazy]
public bool CanUpdateLanguage => IsAllowed(P.AppLanguagesUpdate);
[Lazy]
public bool CanDeleteLanguage => IsAllowed(P.AppLanguagesDelete);
// Patterns
[Lazy]
public bool CanCreatePattern => IsAllowed(P.AppClientsCreate);
[Lazy]
public bool CanUpdatePattern => IsAllowed(P.AppPatternsUpdate);
[Lazy]
public bool CanDeletePattern => IsAllowed(P.AppPatternsDelete);
// Clients
[Lazy]
public bool CanCreateClient => IsAllowed(P.AppClientsCreate);
[Lazy]
public bool CanUpdateClient => IsAllowed(P.AppClientsUpdate);
[Lazy]
public bool CanDeleteClient => IsAllowed(P.AppClientsDelete);
// Rules
[Lazy]
public bool CanDisableRule => IsAllowed(P.AppRulesDisable);
[Lazy]
public bool CanCreateRule => IsAllowed(P.AppRulesCreate);
[Lazy]
public bool CanUpdateRule => IsAllowed(P.AppRulesUpdate);
[Lazy]
public bool CanDeleteRule => IsAllowed(P.AppRulesDelete);
[Lazy]
public bool CanReadRuleEvents => IsAllowed(P.AppRulesEvents);
// Users
[Lazy]
public bool CanReadUsers => IsAllowed(P.AdminUsersRead);
[Lazy]
public bool CanCreateUser => IsAllowed(P.AdminUsersCreate);
[Lazy]
public bool CanLockUser => IsAllowed(P.AdminUsersLock);
[Lazy]
public bool CanUnlockUser => IsAllowed(P.AdminUsersUnlock);
[Lazy]
public bool CanUpdateUser => IsAllowed(P.AdminUsersUpdate);
// Assets
[Lazy]
public bool CanUploadAsset => IsAllowed(P.AppAssetsUpload);
[Lazy]
public bool CanCreateAsset => IsAllowed(P.AppAssetsCreate);
[Lazy]
public bool CanDeleteAsset => IsAllowed(P.AppAssetsDelete);
[Lazy]
public bool CanUpdateAsset => IsAllowed(P.AppAssetsUpdate);
[Lazy]
public bool CanReadAssets => IsAllowed(P.AppAssetsRead);
// Events
[Lazy]
public bool CanReadEvents => IsAllowed(P.AdminEventsRead);
[Lazy]
public bool CanManageEvents => IsAllowed(P.AdminEventsManage);
// Orleans
[Lazy]
public bool CanReadOrleans => IsAllowed(P.AdminOrleans);
// Backups
[Lazy]
public bool CanRestoreBackup => IsAllowed(P.AdminEventsRead);
[Lazy]
public bool CanCreateBackup => IsAllowed(P.AppBackupsCreate);
[Lazy]
public bool CanDeleteBackup => IsAllowed(P.AppBackupsDelete);
[Lazy]
public string? App => GetAppName();
public ApiController Controller { get; }
public PermissionSet Permissions { get; }
public Resources(ApiController controller)
{
Controller = controller;
Permissions = controller.HttpContext.Context().Permissions;
}
public string Url<T>(Func<T?, string> action, object? values = null) where T : ApiController
{
return Controller.Url(action, values);
}
public bool IsUser(string userId)
{
var subject = Controller.User.OpenIdSubject();
return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase);
}
public bool Includes(Permission permission, PermissionSet? additional = null)
{
return Permissions.Includes(permission) || additional?.Includes(permission) == true;
}
public bool IsAllowedForSchema(string id, string schema)
{
return schemaPermissions.GetOrAdd((id, schema), k => IsAllowed(k.Item1, "*", k.Item2));
}
public bool IsAllowed(string id, string app = Permission.Any, string schema = Permission.Any, PermissionSet? additional = null)
{
if (app == Permission.Any)
{
var falback = App;
if (!string.IsNullOrWhiteSpace(falback))
{
app = falback;
}
}
if (schema == Permission.Any)
{
var falback = Controller.HttpContext.Features.Get<ISchemaFeature>()?.SchemaId.Name;
if (!string.IsNullOrWhiteSpace(falback))
{
schema = falback;
}
}
var permission = P.ForApp(id, app, schema);
return Permissions.Allows(permission) || additional?.Allows(permission) == true;
}
private string? GetAppName()
{
return Controller.HttpContext.Context().App?.Name;
}
}
}

5
backend/src/Squidex.Web/Squidex.Web.csproj

@ -12,6 +12,11 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fody" Version="6.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Lazy.Fody" Version="1.8.0" PrivateAssets="all" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup> </ItemGroup>

36
backend/src/Squidex/Areas/Api/Config/IdentityServerPathMiddleware.cs

@ -0,0 +1,36 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using IdentityServer4.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Squidex.Web;
namespace Squidex.Areas.Api.Config
{
public sealed class IdentityServerPathMiddleware
{
private readonly UrlsOptions urlsOptions;
private readonly RequestDelegate next;
public IdentityServerPathMiddleware(IOptions<UrlsOptions> urlsOptions, RequestDelegate next)
{
this.urlsOptions = urlsOptions.Value;
this.next = next;
}
public Task InvokeAsync(HttpContext context)
{
context.SetIdentityServerOrigin(urlsOptions.BaseUrl);
context.SetIdentityServerBasePath(Constants.IdentityServerPrefix);
return next(context);
}
}
}

2
backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -150,7 +150,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
private ClientsDto GetResponse(IAppEntity app) private ClientsDto GetResponse(IAppEntity app)
{ {
return ClientsDto.FromApp(app, this); return ClientsDto.FromApp(app, Resources);
} }
} }
} }

2
backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -125,7 +125,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
private Task<ContributorsDto> GetResponseAsync(IAppEntity app, bool invited) private Task<ContributorsDto> GetResponseAsync(IAppEntity app, bool invited)
{ {
return ContributorsDto.FromAppAsync(app, this, userResolver, appPlansProvider, invited); return ContributorsDto.FromAppAsync(app, Resources, userResolver, appPlansProvider, invited);
} }
} }
} }

2
backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -141,7 +141,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
private AppLanguagesDto GetResponse(IAppEntity result) private AppLanguagesDto GetResponse(IAppEntity result)
{ {
return AppLanguagesDto.FromApp(result, this); return AppLanguagesDto.FromApp(result, Resources);
} }
private static Language ParseLanguage(string language) private static Language ParseLanguage(string language)

2
backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -144,7 +144,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
private PatternsDto GetResponse(IAppEntity result) private PatternsDto GetResponse(IAppEntity result)
{ {
return PatternsDto.FromApp(result, this); return PatternsDto.FromApp(result, Resources);
} }
} }
} }

2
backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs

@ -167,7 +167,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
private RolesDto GetResponse(IAppEntity result) private RolesDto GetResponse(IAppEntity result)
{ {
return RolesDto.FromApp(result, this); return RolesDto.FromApp(result, Resources);
} }
} }
} }

2
backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs

@ -142,7 +142,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
private async Task<WorkflowsDto> GetResponse(IAppEntity result) private async Task<WorkflowsDto> GetResponse(IAppEntity result)
{ {
return await WorkflowsDto.FromAppAsync(workflowsValidator, result, this); return await WorkflowsDto.FromAppAsync(workflowsValidator, result, Resources);
} }
} }
} }

13
backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -73,15 +73,13 @@ namespace Squidex.Areas.Api.Controllers.Apps
public async Task<IActionResult> GetApps() public async Task<IActionResult> GetApps()
{ {
var userOrClientId = HttpContext.User.UserOrClientId()!; var userOrClientId = HttpContext.User.UserOrClientId()!;
var userPermissions = HttpContext.Permissions(); var userPermissions = Resources.Permissions;
var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions); var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions);
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
var userPermissions = HttpContext.Permissions(); return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, appPlansProvider, Resources)).ToArray();
return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)).ToArray();
}); });
Response.Headers[HeaderNames.ETag] = apps.ToEtag(); Response.Headers[HeaderNames.ETag] = apps.ToEtag();
@ -107,9 +105,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
var userOrClientId = HttpContext.User.UserOrClientId()!; var userOrClientId = HttpContext.User.UserOrClientId()!;
var userPermissions = HttpContext.Permissions(); var userPermissions = Resources.Permissions;
return AppDto.FromApp(App, userOrClientId, userPermissions, appPlansProvider, this); return AppDto.FromApp(App, userOrClientId, appPlansProvider, Resources);
}); });
Response.Headers[HeaderNames.ETag] = App.ToEtag(); Response.Headers[HeaderNames.ETag] = App.ToEtag();
@ -299,10 +297,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var userOrClientId = HttpContext.User.UserOrClientId()!; var userOrClientId = HttpContext.User.UserOrClientId()!;
var userPermissions = HttpContext.Permissions();
var result = context.Result<IAppEntity>(); var result = context.Result<IAppEntity>();
var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this); var response = AppDto.FromApp(result, userOrClientId, appPlansProvider, Resources);
return response; return response;
} }

102
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -19,9 +19,8 @@ using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Apps.Plans;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
using AllPermissions = Squidex.Shared.Permissions; using P = Squidex.Shared.Permissions;
#pragma warning disable RECS0033 // Convert 'if' to '||' expression #pragma warning disable RECS0033 // Convert 'if' to '||' expression
@ -91,31 +90,31 @@ 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, PermissionSet userPermissions, IAppPlansProvider plans, ApiController controller) public static AppDto FromApp(IAppEntity app, string userId, IAppPlansProvider plans, Resources resources)
{ {
var permissions = GetPermissions(app, userId, userPermissions); var permissions = GetPermissions(app, userId);
var result = SimpleMapper.Map(app, new AppDto()); var result = SimpleMapper.Map(app, new AppDto());
result.Permissions = permissions.ToIds(); result.Permissions = permissions.ToIds();
if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppApi, app.Name), permissions)) if (resources.Includes(P.ForApp(P.AppApi, app.Name), permissions))
{ {
result.CanAccessApi = true; result.CanAccessApi = true;
} }
if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppContents, app.Name), permissions)) if (resources.Includes(P.ForApp(P.AppContents, app.Name), permissions))
{ {
result.CanAccessContent = true; result.CanAccessContent = true;
} }
result.SetPlan(app, plans, controller, permissions); result.SetPlan(app, plans, resources, permissions);
result.SetImage(app, controller); result.SetImage(app, resources);
return result.CreateLinks(controller, permissions); return result.CreateLinks(resources, permissions);
} }
private static PermissionSet GetPermissions(IAppEntity app, string userId, PermissionSet userPermissions) private static PermissionSet GetPermissions(IAppEntity app, string userId)
{ {
var permissions = new List<Permission>(); var permissions = new List<Permission>();
@ -124,17 +123,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
permissions.AddRange(role.Permissions); permissions.AddRange(role.Permissions);
} }
if (userPermissions != null)
{
permissions.AddRange(userPermissions.ToAppPermissions(app.Name));
}
return new PermissionSet(permissions); return new PermissionSet(permissions);
} }
private void SetPlan(IAppEntity app, IAppPlansProvider plans, ApiController controller, PermissionSet permissions) private void SetPlan(IAppEntity app, IAppPlansProvider plans, Resources resources, PermissionSet permissions)
{ {
if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name, additional: permissions)) if (resources.IsAllowed(P.AppPlansChange, app.Name, additional: permissions))
{ {
PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name; PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;
} }
@ -142,100 +136,100 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
PlanName = plans.GetPlanForApp(app).Plan.Name; PlanName = plans.GetPlanForApp(app).Plan.Name;
} }
private void SetImage(IAppEntity app, ApiController controller) private void SetImage(IAppEntity app, Resources resources)
{ {
if (app.Image != null) if (app.Image != null)
{ {
AddGetLink("image", controller.Url<AppsController>(x => nameof(x.GetImage), new { app = app.Name })); AddGetLink("image", resources.Url<AppsController>(x => nameof(x.GetImage), new { app = app.Name }));
} }
} }
private AppDto CreateLinks(ApiController controller, PermissionSet permissions) private AppDto CreateLinks(Resources resources, PermissionSet permissions)
{ {
var values = new { app = Name }; var values = new { app = Name };
AddGetLink("ping", controller.Url<PingController>(x => nameof(x.GetAppPing), values)); AddGetLink("ping", resources.Url<PingController>(x => nameof(x.GetAppPing), values));
if (controller.HasPermission(AllPermissions.AppDelete, Name, additional: permissions)) if (resources.IsAllowed(P.AppDelete, Name, additional: permissions))
{ {
AddDeleteLink("delete", controller.Url<AppsController>(x => nameof(x.DeleteApp), values)); AddDeleteLink("delete", resources.Url<AppsController>(x => nameof(x.DeleteApp), values));
} }
if (controller.HasPermission(AllPermissions.AppUpdateGeneral, Name, additional: permissions)) if (resources.IsAllowed(P.AppUpdateGeneral, Name, additional: permissions))
{ {
AddPutLink("update", controller.Url<AppsController>(x => nameof(x.UpdateApp), values)); AddPutLink("update", resources.Url<AppsController>(x => nameof(x.UpdateApp), values));
} }
if (controller.HasPermission(AllPermissions.AppUpdateImage, Name, additional: permissions)) if (resources.IsAllowed(P.AppUpdateImage, Name, additional: permissions))
{ {
AddPostLink("image/upload", controller.Url<AppsController>(x => nameof(x.UploadImage), values)); AddPostLink("image/upload", resources.Url<AppsController>(x => nameof(x.UploadImage), values));
AddDeleteLink("image/delete", controller.Url<AppsController>(x => nameof(x.DeleteImage), values)); AddDeleteLink("image/delete", resources.Url<AppsController>(x => nameof(x.DeleteImage), values));
} }
if (controller.HasPermission(AllPermissions.AppAssetsRead, Name, additional: permissions)) if (resources.IsAllowed(P.AppAssetsRead, Name, additional: permissions))
{ {
AddGetLink("assets", controller.Url<AssetsController>(x => nameof(x.GetAssets), values)); AddGetLink("assets", resources.Url<AssetsController>(x => nameof(x.GetAssets), values));
} }
if (controller.HasPermission(AllPermissions.AppBackupsRead, Name, additional: permissions)) if (resources.IsAllowed(P.AppBackupsRead, Name, additional: permissions))
{ {
AddGetLink("backups", controller.Url<BackupsController>(x => nameof(x.GetBackups), values)); AddGetLink("backups", resources.Url<BackupsController>(x => nameof(x.GetBackups), values));
} }
if (controller.HasPermission(AllPermissions.AppClientsRead, Name, additional: permissions)) if (resources.IsAllowed(P.AppClientsRead, Name, additional: permissions))
{ {
AddGetLink("clients", controller.Url<AppClientsController>(x => nameof(x.GetClients), values)); AddGetLink("clients", resources.Url<AppClientsController>(x => nameof(x.GetClients), values));
} }
if (controller.HasPermission(AllPermissions.AppContributorsRead, Name, additional: permissions)) if (resources.IsAllowed(P.AppContributorsRead, Name, additional: permissions))
{ {
AddGetLink("contributors", controller.Url<AppContributorsController>(x => nameof(x.GetContributors), values)); AddGetLink("contributors", resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
} }
if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) if (resources.IsAllowed(P.AppCommon, Name, additional: permissions))
{ {
AddGetLink("languages", controller.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values)); AddGetLink("languages", resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values));
} }
if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) if (resources.IsAllowed(P.AppCommon, Name, additional: permissions))
{ {
AddGetLink("patterns", controller.Url<AppPatternsController>(x => nameof(x.GetPatterns), values)); AddGetLink("patterns", resources.Url<AppPatternsController>(x => nameof(x.GetPatterns), values));
} }
if (controller.HasPermission(AllPermissions.AppPlansRead, Name, additional: permissions)) if (resources.IsAllowed(P.AppPlansRead, Name, additional: permissions))
{ {
AddGetLink("plans", controller.Url<AppPlansController>(x => nameof(x.GetPlans), values)); AddGetLink("plans", resources.Url<AppPlansController>(x => nameof(x.GetPlans), values));
} }
if (controller.HasPermission(AllPermissions.AppRolesRead, Name, additional: permissions)) if (resources.IsAllowed(P.AppRolesRead, Name, additional: permissions))
{ {
AddGetLink("roles", controller.Url<AppRolesController>(x => nameof(x.GetRoles), values)); AddGetLink("roles", resources.Url<AppRolesController>(x => nameof(x.GetRoles), values));
} }
if (controller.HasPermission(AllPermissions.AppRulesRead, Name, additional: permissions)) if (resources.IsAllowed(P.AppRulesRead, Name, additional: permissions))
{ {
AddGetLink("rules", controller.Url<RulesController>(x => nameof(x.GetRules), values)); AddGetLink("rules", resources.Url<RulesController>(x => nameof(x.GetRules), values));
} }
if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) if (resources.IsAllowed(P.AppCommon, Name, additional: permissions))
{ {
AddGetLink("schemas", controller.Url<SchemasController>(x => nameof(x.GetSchemas), values)); AddGetLink("schemas", resources.Url<SchemasController>(x => nameof(x.GetSchemas), values));
} }
if (controller.HasPermission(AllPermissions.AppWorkflowsRead, Name, additional: permissions)) if (resources.IsAllowed(P.AppWorkflowsRead, Name, additional: permissions))
{ {
AddGetLink("workflows", controller.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values)); AddGetLink("workflows", resources.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values));
} }
if (controller.HasPermission(AllPermissions.AppSchemasCreate, Name, additional: permissions)) if (resources.IsAllowed(P.AppSchemasCreate, Name, additional: permissions))
{ {
AddPostLink("schemas/create", controller.Url<SchemasController>(x => nameof(x.PostSchema), values)); AddPostLink("schemas/create", resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
} }
if (controller.HasPermission(AllPermissions.AppAssetsCreate, Name, additional: permissions)) if (resources.IsAllowed(P.AppAssetsCreate, Name, additional: permissions))
{ {
AddPostLink("assets/create", controller.Url<SchemasController>(x => nameof(x.PostSchema), values)); AddPostLink("assets/create", resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
} }
return this; return this;

11
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs

@ -10,7 +10,6 @@ using System.Linq;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -59,20 +58,20 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
return result; return result;
} }
public AppLanguageDto WithLinks(ApiController controller, IAppEntity app) public AppLanguageDto WithLinks(Resources resources, IAppEntity app)
{ {
var values = new { app = app.Name, language = Iso2Code }; var values = new { app = app.Name, language = Iso2Code };
if (!IsMaster) if (!IsMaster)
{ {
if (controller.HasPermission(Permissions.AppLanguagesUpdate, app.Name)) if (resources.CanUpdateLanguage)
{ {
AddPutLink("update", controller.Url<AppLanguagesController>(x => nameof(x.PutLanguage), values)); AddPutLink("update", resources.Url<AppLanguagesController>(x => nameof(x.PutLanguage), values));
} }
if (controller.HasPermission(Permissions.AppLanguagesDelete, app.Name) && app.LanguagesConfig.Languages.Count > 1) if (resources.CanDeleteLanguage && app.LanguagesConfig.Languages.Count > 1)
{ {
AddDeleteLink("delete", controller.Url<AppLanguagesController>(x => nameof(x.DeleteLanguage), values)); AddDeleteLink("delete", resources.Url<AppLanguagesController>(x => nameof(x.DeleteLanguage), values));
} }
} }

17
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs

@ -8,7 +8,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -21,7 +20,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required] [Required]
public AppLanguageDto[] Items { get; set; } public AppLanguageDto[] Items { get; set; }
public static AppLanguagesDto FromApp(IAppEntity app, ApiController controller) public static AppLanguagesDto FromApp(IAppEntity app, Resources resources)
{ {
var config = app.LanguagesConfig; var config = app.LanguagesConfig;
@ -29,23 +28,23 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
Items = config.Languages Items = config.Languages
.Select(x => AppLanguageDto.FromLanguage(x.Key, x.Value, config)) .Select(x => AppLanguageDto.FromLanguage(x.Key, x.Value, config))
.Select(x => x.WithLinks(controller, app)) .Select(x => x.WithLinks(resources, app))
.OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code) .OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code)
.ToArray() .ToArray()
}; };
return result.CreateLinks(controller, app.Name); return result.CreateLinks(resources);
} }
private AppLanguagesDto CreateLinks(ApiController controller, string app) private AppLanguagesDto CreateLinks(Resources resources)
{ {
var values = new { app }; var values = new { app = resources.App };
AddSelfLink(controller.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values)); AddSelfLink(resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values));
if (controller.HasPermission(Permissions.AppLanguagesCreate, app)) if (resources.CanCreateLanguage)
{ {
AddPostLink("create", controller.Url<AppLanguagesController>(x => nameof(x.PostLanguage), values)); AddPostLink("create", resources.Url<AppLanguagesController>(x => nameof(x.PostLanguage), values));
} }
return this; return this;

11
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs

@ -8,7 +8,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -45,18 +44,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
return result; return result;
} }
public ClientDto WithLinks(ApiController controller, string app) public ClientDto WithLinks(Resources resources, string app)
{ {
var values = new { app, id = Id }; var values = new { app, id = Id };
if (controller.HasPermission(Permissions.AppClientsUpdate, app)) if (resources.CanUpdateClient)
{ {
AddPutLink("update", controller.Url<AppClientsController>(x => nameof(x.PutClient), values)); AddPutLink("update", resources.Url<AppClientsController>(x => nameof(x.PutClient), values));
} }
if (controller.HasPermission(Permissions.AppClientsDelete, app)) if (resources.CanDeleteClient)
{ {
AddDeleteLink("delete", controller.Url<AppClientsController>(x => nameof(x.DeleteClient), values)); AddDeleteLink("delete", resources.Url<AppClientsController>(x => nameof(x.DeleteClient), values));
} }
return this; return this;

15
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs

@ -8,7 +8,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -21,7 +20,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required] [Required]
public ClientDto[] Items { get; set; } public ClientDto[] Items { get; set; }
public static ClientsDto FromApp(IAppEntity app, ApiController controller) public static ClientsDto FromApp(IAppEntity app, Resources resources)
{ {
var appName = app.Name; var appName = app.Name;
@ -30,22 +29,22 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
Items = Items =
app.Clients app.Clients
.Select(x => ClientDto.FromClient(x.Key, x.Value)) .Select(x => ClientDto.FromClient(x.Key, x.Value))
.Select(x => x.WithLinks(controller, appName)) .Select(x => x.WithLinks(resources, appName))
.ToArray() .ToArray()
}; };
return result.CreateLinks(controller, appName); return result.CreateLinks(resources, appName);
} }
private ClientsDto CreateLinks(ApiController controller, string app) private ClientsDto CreateLinks(Resources resources, string app)
{ {
var values = new { app }; var values = new { app };
AddSelfLink(controller.Url<AppClientsController>(x => nameof(x.GetClients), values)); AddSelfLink(resources.Url<AppClientsController>(x => nameof(x.GetClients), values));
if (controller.HasPermission(Permissions.AppClientsCreate, app)) if (resources.CanCreateClient)
{ {
AddPostLink("create", controller.Url<AppClientsController>(x => nameof(x.PostClient), values)); AddPostLink("create", resources.Url<AppClientsController>(x => nameof(x.PostClient), values));
} }
return this; return this;

15
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs

@ -7,7 +7,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Shared;
using Squidex.Shared.Users; using Squidex.Shared.Users;
using Squidex.Web; using Squidex.Web;
@ -62,18 +61,20 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
return this; return this;
} }
public ContributorDto WithLinks(ApiController controller, string app) public ContributorDto WithLinks(Resources resources)
{ {
if (!controller.IsUser(ContributorId)) if (!resources.IsUser(ContributorId))
{ {
if (controller.HasPermission(Permissions.AppContributorsAssign, app)) var app = resources.App;
if (resources.CanAssignContributor)
{ {
AddPostLink("update", controller.Url<AppContributorsController>(x => nameof(x.PostContributor), new { app })); AddPostLink("update", resources.Url<AppContributorsController>(x => nameof(x.PostContributor), new { app }));
} }
if (controller.HasPermission(Permissions.AppContributorsRevoke, app)) if (resources.CanRevokeContributor)
{ {
AddDeleteLink("delete", controller.Url<AppContributorsController>(x => nameof(x.DeleteContributor), new { app, id = ContributorId })); AddDeleteLink("delete", resources.Url<AppContributorsController>(x => nameof(x.DeleteContributor), new { app, id = ContributorId }));
} }
} }

15
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs

@ -11,7 +11,6 @@ using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Apps.Plans;
using Squidex.Shared;
using Squidex.Shared.Users; using Squidex.Shared.Users;
using Squidex.Web; using Squidex.Web;
@ -36,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[JsonProperty("_meta")] [JsonProperty("_meta")]
public ContributorsMetadata Metadata { get; set; } public ContributorsMetadata Metadata { get; set; }
public static async Task<ContributorsDto> FromAppAsync(IAppEntity app, ApiController controller, IUserResolver userResolver, IAppPlansProvider plans, bool invited) public static async Task<ContributorsDto> FromAppAsync(IAppEntity app, Resources resources, IUserResolver userResolver, IAppPlansProvider plans, bool invited)
{ {
var users = await userResolver.QueryManyAsync(app.Contributors.Keys.ToArray()); var users = await userResolver.QueryManyAsync(app.Contributors.Keys.ToArray());
@ -46,7 +45,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
app.Contributors app.Contributors
.Select(x => ContributorDto.FromIdAndRole(x.Key, x.Value)) .Select(x => ContributorDto.FromIdAndRole(x.Key, x.Value))
.Select(x => x.WithUser(users)) .Select(x => x.WithUser(users))
.Select(x => x.WithLinks(controller, app.Name)) .Select(x => x.WithLinks(resources))
.OrderBy(x => x.ContributorName) .OrderBy(x => x.ContributorName)
.ToArray() .ToArray()
}; };
@ -54,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
result.WithInvited(invited); result.WithInvited(invited);
result.WithPlan(app, plans); result.WithPlan(app, plans);
return result.CreateLinks(controller, app.Name); return result.CreateLinks(resources, app.Name);
} }
private void WithPlan(IAppEntity app, IAppPlansProvider plans) private void WithPlan(IAppEntity app, IAppPlansProvider plans)
@ -73,15 +72,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
} }
} }
private ContributorsDto CreateLinks(ApiController controller, string app) private ContributorsDto CreateLinks(Resources resources, string app)
{ {
var values = new { app }; var values = new { app };
AddSelfLink(controller.Url<AppContributorsController>(x => nameof(x.GetContributors), values)); AddSelfLink(resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
if (controller.HasPermission(Permissions.AppContributorsAssign, app) && (MaxContributors < 0 || Items.Length < MaxContributors)) if (resources.CanAssignContributor && (MaxContributors < 0 || Items.Length < MaxContributors))
{ {
AddPostLink("create", controller.Url<AppContributorsController>(x => nameof(x.PostContributor), values)); AddPostLink("create", resources.Url<AppContributorsController>(x => nameof(x.PostContributor), values));
} }
return this; return this;

17
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs

@ -9,7 +9,6 @@ using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -38,25 +37,25 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
public string? Message { get; set; } public string? Message { get; set; }
public static PatternDto FromPattern(Guid id, AppPattern pattern, ApiController controller, string app) public static PatternDto FromPattern(Guid id, AppPattern pattern, Resources resources)
{ {
var result = SimpleMapper.Map(pattern, new PatternDto { Id = id }); var result = SimpleMapper.Map(pattern, new PatternDto { Id = id });
return result.CreateLinks(controller, app); return result.CreateLinks(resources);
} }
private PatternDto CreateLinks(ApiController controller, string app) private PatternDto CreateLinks(Resources resources)
{ {
var values = new { app, id = Id }; var values = new { app = resources.App, id = Id };
if (controller.HasPermission(Permissions.AppPatternsUpdate, app)) if (resources.CanUpdatePattern)
{ {
AddPutLink("update", controller.Url<AppPatternsController>(x => nameof(x.PutPattern), values)); AddPutLink("update", resources.Url<AppPatternsController>(x => nameof(x.PutPattern), values));
} }
if (controller.HasPermission(Permissions.AppPatternsDelete, app)) if (resources.CanDeletePattern)
{ {
AddDeleteLink("delete", controller.Url<AppPatternsController>(x => nameof(x.DeletePattern), values)); AddDeleteLink("delete", resources.Url<AppPatternsController>(x => nameof(x.DeletePattern), values));
} }
return this; return this;

17
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs

@ -8,7 +8,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -21,25 +20,25 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required] [Required]
public PatternDto[] Items { get; set; } public PatternDto[] Items { get; set; }
public static PatternsDto FromApp(IAppEntity app, ApiController controller) public static PatternsDto FromApp(IAppEntity app, Resources resources)
{ {
var result = new PatternsDto var result = new PatternsDto
{ {
Items = app.Patterns.Select(x => PatternDto.FromPattern(x.Key, x.Value, controller, app.Name)).ToArray() Items = app.Patterns.Select(x => PatternDto.FromPattern(x.Key, x.Value, resources)).ToArray()
}; };
return result.CreateLinks(controller, app.Name); return result.CreateLinks(resources);
} }
private PatternsDto CreateLinks(ApiController controller, string app) private PatternsDto CreateLinks(Resources resources)
{ {
var values = new { app }; var values = new { app = resources.App };
AddSelfLink(controller.Url<AppPatternsController>(x => nameof(x.GetPatterns), values)); AddSelfLink(resources.Url<AppPatternsController>(x => nameof(x.GetPatterns), values));
if (controller.HasPermission(Permissions.AppPatternsCreate, app)) if (resources.CanCreatePattern)
{ {
AddPostLink("create", controller.Url<AppPatternsController>(x => nameof(x.PostPattern), values)); AddPostLink("create", resources.Url<AppPatternsController>(x => nameof(x.PostPattern), values));
} }
return this; return this;

13
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs

@ -11,7 +11,6 @@ using System.Linq;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Web; using Squidex.Web;
using AllPermissions = Squidex.Shared.Permissions;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -70,20 +69,20 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
return app.Clients.Count(x => role.Equals(x.Value.Role)); return app.Clients.Count(x => role.Equals(x.Value.Role));
} }
public RoleDto WithLinks(ApiController controller, string app) public RoleDto WithLinks(Resources resources)
{ {
var values = new { app, name = Name }; var values = new { app = resources.App, name = Name };
if (!IsDefaultRole) if (!IsDefaultRole)
{ {
if (controller.HasPermission(AllPermissions.AppRolesUpdate, app)) if (resources.CanUpdateRole)
{ {
AddPutLink("update", controller.Url<AppRolesController>(x => nameof(x.PutRole), values)); AddPutLink("update", resources.Url<AppRolesController>(x => nameof(x.PutRole), values));
} }
if (controller.HasPermission(AllPermissions.AppRolesDelete, app) && NumClients == 0 && NumContributors == 0) if (resources.CanDeleteRole && NumClients == 0 && NumContributors == 0)
{ {
AddDeleteLink("delete", controller.Url<AppRolesController>(x => nameof(x.DeleteRole), values)); AddDeleteLink("delete", resources.Url<AppRolesController>(x => nameof(x.DeleteRole), values));
} }
} }

19
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs

@ -8,7 +8,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -21,32 +20,30 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required] [Required]
public RoleDto[] Items { get; set; } public RoleDto[] Items { get; set; }
public static RolesDto FromApp(IAppEntity app, ApiController controller) public static RolesDto FromApp(IAppEntity app, Resources resources)
{ {
var appName = app.Name;
var result = new RolesDto var result = new RolesDto
{ {
Items = Items =
app.Roles.All app.Roles.All
.Select(x => RoleDto.FromRole(x, app)) .Select(x => RoleDto.FromRole(x, app))
.Select(x => x.WithLinks(controller, appName)) .Select(x => x.WithLinks(resources))
.OrderBy(x => x.Name) .OrderBy(x => x.Name)
.ToArray() .ToArray()
}; };
return result.CreateLinks(controller, appName); return result.CreateLinks(resources);
} }
private RolesDto CreateLinks(ApiController controller, string app) private RolesDto CreateLinks(Resources resources)
{ {
var values = new { app }; var values = new { app = resources.App };
AddSelfLink(controller.Url<AppRolesController>(x => nameof(x.GetRoles), values)); AddSelfLink(resources.Url<AppRolesController>(x => nameof(x.GetRoles), values));
if (controller.HasPermission(Permissions.AppRolesCreate, app)) if (resources.CanCreateRole)
{ {
AddPostLink("create", controller.Url<AppRolesController>(x => nameof(x.PostRole), values)); AddPostLink("create", resources.Url<AppRolesController>(x => nameof(x.PostRole), values));
} }
return this; return this;

13
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs

@ -11,7 +11,6 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -57,18 +56,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
return result; return result;
} }
public WorkflowDto WithLinks(ApiController controller, string app) public WorkflowDto WithLinks(Resources resources)
{ {
var values = new { app, id = Id }; var values = new { app = resources.App, id = Id };
if (controller.HasPermission(Permissions.AppWorkflowsUpdate, app)) if (resources.CanUpdateWorkflow)
{ {
AddPutLink("update", controller.Url<AppWorkflowsController>(x => nameof(x.PutWorkflow), values)); AddPutLink("update", resources.Url<AppWorkflowsController>(x => nameof(x.PutWorkflow), values));
} }
if (controller.HasPermission(Permissions.AppWorkflowsDelete, app)) if (resources.CanDeleteWorkflow)
{ {
AddDeleteLink("delete", controller.Url<AppWorkflowsController>(x => nameof(x.DeleteWorkflow), values)); AddDeleteLink("delete", resources.Url<AppWorkflowsController>(x => nameof(x.DeleteWorkflow), values));
} }
return this; return this;

19
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs

@ -10,7 +10,6 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -29,16 +28,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required] [Required]
public string[] Errors { get; set; } public string[] Errors { get; set; }
public static async Task<WorkflowsDto> FromAppAsync(IWorkflowsValidator workflowsValidator, IAppEntity app, ApiController controller) public static async Task<WorkflowsDto> FromAppAsync(IWorkflowsValidator workflowsValidator, IAppEntity app, Resources resources)
{ {
var appName = app.Name;
var result = new WorkflowsDto var result = new WorkflowsDto
{ {
Items = Items =
app.Workflows app.Workflows
.Select(x => WorkflowDto.FromWorkflow(x.Key, x.Value)) .Select(x => WorkflowDto.FromWorkflow(x.Key, x.Value))
.Select(x => x.WithLinks(controller, appName)) .Select(x => x.WithLinks(resources))
.ToArray() .ToArray()
}; };
@ -46,18 +43,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
result.Errors = errors.ToArray(); result.Errors = errors.ToArray();
return result.CreateLinks(controller, appName); return result.CreateLinks(resources);
} }
private WorkflowsDto CreateLinks(ApiController controller, string app) private WorkflowsDto CreateLinks(Resources resources)
{ {
var values = new { app }; var values = new { app = resources.App };
AddSelfLink(controller.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values)); AddSelfLink(resources.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values));
if (controller.HasPermission(Permissions.AppWorkflowsCreate, app)) if (resources.CanCreateWorkflow)
{ {
AddPostLink("create", controller.Url<AppWorkflowsController>(x => nameof(x.PostWorkflow), values)); AddPostLink("create", resources.Url<AppWorkflowsController>(x => nameof(x.PostWorkflow), values));
} }
return this; return this;

3
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -20,7 +20,6 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
#pragma warning disable 1573 #pragma warning disable 1573
@ -119,7 +118,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
return NotFound(); return NotFound();
} }
if (asset.IsProtected && !this.HasPermission(Permissions.AppAssetsRead)) if (asset.IsProtected && !Resources.CanReadEvents)
{ {
return StatusCode(403); return StatusCode(403);
} }

4
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs

@ -55,7 +55,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
return AssetFoldersDto.FromAssets(assetFolders, this, app); return AssetFoldersDto.FromAssets(assetFolders, Resources);
}); });
Response.Headers[HeaderNames.ETag] = assetFolders.ToEtag(); Response.Headers[HeaderNames.ETag] = assetFolders.ToEtag();
@ -162,7 +162,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
{ {
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
return AssetFolderDto.FromAssetFolder(context.Result<IAssetFolderEntity>(), this, app); return AssetFolderDto.FromAssetFolder(context.Result<IAssetFolderEntity>(), Resources);
} }
} }
} }

10
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -100,7 +100,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
return AssetsDto.FromAssets(assets, this, app); return AssetsDto.FromAssets(assets, Resources);
}); });
return Ok(response); return Ok(response);
@ -129,7 +129,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
return AssetsDto.FromAssets(assets, this, app); return AssetsDto.FromAssets(assets, Resources);
}); });
return Ok(response); return Ok(response);
@ -160,7 +160,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
return AssetDto.FromAsset(asset, this, app); return AssetDto.FromAsset(asset, Resources);
}); });
return Ok(response); return Ok(response);
@ -304,11 +304,11 @@ namespace Squidex.Areas.Api.Controllers.Assets
if (context.PlainResult is AssetCreatedResult created) if (context.PlainResult is AssetCreatedResult created)
{ {
return AssetDto.FromAsset(created.Asset, this, app, created.IsDuplicate); return AssetDto.FromAsset(created.Asset, Resources, created.IsDuplicate);
} }
else else
{ {
return AssetDto.FromAsset(context.Result<IEnrichedAssetEntity>(), this, app); return AssetDto.FromAsset(context.Result<IEnrichedAssetEntity>(), Resources);
} }
} }

32
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -14,7 +14,6 @@ using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models namespace Squidex.Areas.Api.Controllers.Assets.Models
@ -158,7 +157,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
get { return Metadata.GetPixelHeight(); } get { return Metadata.GetPixelHeight(); }
} }
public static AssetDto FromAsset(IEnrichedAssetEntity asset, ApiController controller, string app, bool isDuplicate = false) public static AssetDto FromAsset(IEnrichedAssetEntity asset, Resources resources, bool isDuplicate = false)
{ {
var response = SimpleMapper.Map(asset, new AssetDto()); var response = SimpleMapper.Map(asset, new AssetDto());
@ -174,42 +173,45 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
}; };
} }
return CreateLinks(response, controller, app); return CreateLinks(response, resources);
} }
private static AssetDto CreateLinks(AssetDto response, ApiController controller, string app) private static AssetDto CreateLinks(AssetDto response, Resources resources)
{ {
var app = resources.App;
var values = new { app, id = response.Id }; var values = new { app, id = response.Id };
response.AddSelfLink(controller.Url<AssetsController>(x => nameof(x.GetAsset), values)); response.AddSelfLink(resources.Url<AssetsController>(x => nameof(x.GetAsset), values));
if (controller.HasPermission(Permissions.AppAssetsUpdate)) if (resources.CanUpdateAsset)
{ {
response.AddPutLink("update", controller.Url<AssetsController>(x => nameof(x.PutAsset), values)); response.AddPutLink("update", resources.Url<AssetsController>(x => nameof(x.PutAsset), values));
response.AddPutLink("move", controller.Url<AssetsController>(x => nameof(x.PutAssetParent), values)); response.AddPutLink("move", resources.Url<AssetsController>(x => nameof(x.PutAssetParent), values));
} }
if (controller.HasPermission(Permissions.AppAssetsUpload)) if (resources.CanUploadAsset)
{ {
response.AddPutLink("upload", controller.Url<AssetsController>(x => nameof(x.PutAssetContent), values)); response.AddPutLink("upload", resources.Url<AssetsController>(x => nameof(x.PutAssetContent), values));
} }
if (controller.HasPermission(Permissions.AppAssetsDelete)) if (resources.CanDeleteAsset)
{ {
response.AddDeleteLink("delete", controller.Url<AssetsController>(x => nameof(x.DeleteAsset), values)); response.AddDeleteLink("delete", resources.Url<AssetsController>(x => nameof(x.DeleteAsset), values));
} }
var version = response.FileVersion; var version = response.FileVersion;
if (!string.IsNullOrWhiteSpace(response.Slug)) if (!string.IsNullOrWhiteSpace(response.Slug))
{ {
response.AddGetLink("content", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Id, more = response.Slug })); response.AddGetLink("content", resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Id, more = response.Slug }));
response.AddGetLink("content/slug", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Slug }));
response.AddGetLink("content/slug", resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Slug }));
} }
else else
{ {
response.AddGetLink("content", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Id })); response.AddGetLink("content", resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Id }));
} }
return response; return response;

21
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs

@ -9,7 +9,6 @@ using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models namespace Squidex.Areas.Api.Controllers.Assets.Models
@ -37,29 +36,29 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary> /// </summary>
public long Version { get; set; } public long Version { get; set; }
public static AssetFolderDto FromAssetFolder(IAssetFolderEntity asset, ApiController controller, string app) public static AssetFolderDto FromAssetFolder(IAssetFolderEntity asset, Resources resources)
{ {
var response = SimpleMapper.Map(asset, new AssetFolderDto()); var response = SimpleMapper.Map(asset, new AssetFolderDto());
return CreateLinks(response, controller, app); return CreateLinks(response, resources);
} }
private static AssetFolderDto CreateLinks(AssetFolderDto response, ApiController controller, string app) private static AssetFolderDto CreateLinks(AssetFolderDto response, Resources resources)
{ {
var values = new { app, id = response.Id }; var values = new { app = resources.App, id = response.Id };
response.AddSelfLink(controller.Url<AssetsController>(x => nameof(x.GetAsset), values)); response.AddSelfLink(resources.Url<AssetsController>(x => nameof(x.GetAsset), values));
if (controller.HasPermission(Permissions.AppAssetsUpdate)) if (resources.CanUpdateAsset)
{ {
response.AddPutLink("update", controller.Url<AssetFoldersController>(x => nameof(x.PutAssetFolder), values)); response.AddPutLink("update", resources.Url<AssetFoldersController>(x => nameof(x.PutAssetFolder), values));
response.AddPutLink("move", controller.Url<AssetFoldersController>(x => nameof(x.PutAssetFolderParent), values)); response.AddPutLink("move", resources.Url<AssetFoldersController>(x => nameof(x.PutAssetFolderParent), values));
} }
if (controller.HasPermission(Permissions.AppAssetsUpdate)) if (resources.CanUpdateAsset)
{ {
response.AddDeleteLink("delete", controller.Url<AssetFoldersController>(x => nameof(x.DeleteAssetFolder), values)); response.AddDeleteLink("delete", resources.Url<AssetFoldersController>(x => nameof(x.DeleteAssetFolder), values));
} }
return response; return response;

17
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs

@ -9,7 +9,6 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models namespace Squidex.Areas.Api.Controllers.Assets.Models
@ -27,26 +26,26 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[Required] [Required]
public AssetFolderDto[] Items { get; set; } public AssetFolderDto[] Items { get; set; }
public static AssetFoldersDto FromAssets(IResultList<IAssetFolderEntity> assetFolders, ApiController controller, string app) public static AssetFoldersDto FromAssets(IResultList<IAssetFolderEntity> assetFolders, Resources resources)
{ {
var response = new AssetFoldersDto var response = new AssetFoldersDto
{ {
Total = assetFolders.Total, Total = assetFolders.Total,
Items = assetFolders.Select(x => AssetFolderDto.FromAssetFolder(x, controller, app)).ToArray() Items = assetFolders.Select(x => AssetFolderDto.FromAssetFolder(x, resources)).ToArray()
}; };
return CreateLinks(response, controller, app); return CreateLinks(response, resources);
} }
private static AssetFoldersDto CreateLinks(AssetFoldersDto response, ApiController controller, string app) private static AssetFoldersDto CreateLinks(AssetFoldersDto response, Resources resources)
{ {
var values = new { app }; var values = new { app = resources.App };
response.AddSelfLink(controller.Url<AssetFoldersController>(x => nameof(x.GetAssetFolders), values)); response.AddSelfLink(resources.Url<AssetFoldersController>(x => nameof(x.GetAssetFolders), values));
if (controller.HasPermission(Permissions.AppAssetsUpdate)) if (resources.CanUpdateAsset)
{ {
response.AddPostLink("create", controller.Url<AssetFoldersController>(x => nameof(x.PostAssetFolder), values)); response.AddPostLink("create", resources.Url<AssetFoldersController>(x => nameof(x.PostAssetFolder), values));
} }
return response; return response;

19
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs

@ -9,7 +9,6 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models namespace Squidex.Areas.Api.Controllers.Assets.Models
@ -27,29 +26,29 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[Required] [Required]
public AssetDto[] Items { get; set; } public AssetDto[] Items { get; set; }
public static AssetsDto FromAssets(IResultList<IEnrichedAssetEntity> assets, ApiController controller, string app) public static AssetsDto FromAssets(IResultList<IEnrichedAssetEntity> assets, Resources resources)
{ {
var response = new AssetsDto var response = new AssetsDto
{ {
Total = assets.Total, Total = assets.Total,
Items = assets.Select(x => AssetDto.FromAsset(x, controller, app)).ToArray() Items = assets.Select(x => AssetDto.FromAsset(x, resources)).ToArray()
}; };
return CreateLinks(response, controller, app); return CreateLinks(response, resources);
} }
private static AssetsDto CreateLinks(AssetsDto response, ApiController controller, string app) private static AssetsDto CreateLinks(AssetsDto response, Resources resources)
{ {
var values = new { app }; var values = new { app = resources.App };
response.AddSelfLink(controller.Url<AssetsController>(x => nameof(x.GetAssets), values)); response.AddSelfLink(resources.Url<AssetsController>(x => nameof(x.GetAssets), values));
if (controller.HasPermission(Permissions.AppAssetsCreate)) if (resources.CanCreateAsset)
{ {
response.AddPostLink("create", controller.Url<AssetsController>(x => nameof(x.PostAsset), values)); response.AddPostLink("create", resources.Url<AssetsController>(x => nameof(x.PostAsset), values));
} }
response.AddGetLink("tags", controller.Url<AssetsController>(x => nameof(x.GetTags), values)); response.AddGetLink("tags", resources.Url<AssetsController>(x => nameof(x.GetTags), values));
return response; return response;
} }

2
backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs

@ -50,7 +50,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
{ {
var jobs = await backupService.GetBackupsAsync(AppId); var jobs = await backupService.GetBackupsAsync(AppId);
var response = BackupJobsDto.FromBackups(jobs, this, app); var response = BackupJobsDto.FromBackups(jobs, Resources);
return Ok(response); return Ok(response);
} }

15
backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs

@ -9,7 +9,6 @@ using System;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Backups.Models namespace Squidex.Areas.Api.Controllers.Backups.Models
@ -46,23 +45,23 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models
/// </summary> /// </summary>
public JobStatus Status { get; set; } public JobStatus Status { get; set; }
public static BackupJobDto FromBackup(IBackupJob backup, ApiController controller, string app) public static BackupJobDto FromBackup(IBackupJob backup, Resources resources)
{ {
var result = SimpleMapper.Map(backup, new BackupJobDto()); var result = SimpleMapper.Map(backup, new BackupJobDto());
return result.CreateLinks(controller, app); return result.CreateLinks(resources);
} }
private BackupJobDto CreateLinks(ApiController controller, string app) private BackupJobDto CreateLinks(Resources resources)
{ {
var values = new { app, id = Id }; var values = new { app = resources.App, id = Id };
if (controller.HasPermission(Permissions.AppBackupsDelete, app)) if (resources.CanDeleteBackup)
{ {
AddDeleteLink("delete", controller.Url<BackupsController>(x => nameof(x.DeleteBackup), values)); AddDeleteLink("delete", resources.Url<BackupsController>(x => nameof(x.DeleteBackup), values));
} }
AddGetLink("download", controller.Url<BackupContentController>(x => nameof(x.GetBackupContent), values)); AddGetLink("download", resources.Url<BackupContentController>(x => nameof(x.GetBackupContent), values));
return this; return this;
} }

17
backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs

@ -9,7 +9,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Backups.Models namespace Squidex.Areas.Api.Controllers.Backups.Models
@ -22,25 +21,25 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models
[Required] [Required]
public BackupJobDto[] Items { get; set; } public BackupJobDto[] Items { get; set; }
public static BackupJobsDto FromBackups(IEnumerable<IBackupJob> backups, ApiController controller, string app) public static BackupJobsDto FromBackups(IEnumerable<IBackupJob> backups, Resources resources)
{ {
var result = new BackupJobsDto var result = new BackupJobsDto
{ {
Items = backups.Select(x => BackupJobDto.FromBackup(x, controller, app)).ToArray() Items = backups.Select(x => BackupJobDto.FromBackup(x, resources)).ToArray()
}; };
return result.CreateLinks(controller, app); return result.CreateLinks(resources);
} }
private BackupJobsDto CreateLinks(ApiController controller, string app) private BackupJobsDto CreateLinks(Resources resources)
{ {
var values = new { app }; var values = new { app = resources.App };
AddSelfLink(controller.Url<BackupsController>(x => nameof(x.GetBackups), values)); AddSelfLink(resources.Url<BackupsController>(x => nameof(x.GetBackups), values));
if (controller.HasPermission(Permissions.AppBackupsCreate, app)) if (resources.CanCreateBackup)
{ {
AddPostLink("create", controller.Url<BackupsController>(x => nameof(x.PostBackup), values)); AddPostLink("create", resources.Url<BackupsController>(x => nameof(x.PostBackup), values));
} }
return this; return this;

32
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -124,7 +124,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var response = Deferred.AsyncResponse(() => var response = Deferred.AsyncResponse(() =>
{ {
return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow); return ContentsDto.FromContentsAsync(contents, Context, Resources, null, contentWorkflow);
}); });
return Ok(response); return Ok(response);
@ -153,7 +153,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var response = Deferred.AsyncResponse(() => var response = Deferred.AsyncResponse(() =>
{ {
return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow); return ContentsDto.FromContentsAsync(contents, Context, Resources, null, contentWorkflow);
}); });
return Ok(response); return Ok(response);
@ -186,7 +186,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var response = Deferred.AsyncResponse(async () => var response = Deferred.AsyncResponse(async () =>
{ {
return await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow); return await ContentsDto.FromContentsAsync(contents, Context, Resources, schema, contentWorkflow);
}); });
return Ok(response); return Ok(response);
@ -218,7 +218,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var response = Deferred.AsyncResponse(async () => var response = Deferred.AsyncResponse(async () =>
{ {
return await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow); return await ContentsDto.FromContentsAsync(contents, Context, Resources, schema, contentWorkflow);
}); });
return Ok(response); return Ok(response);
@ -246,7 +246,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
var content = await contentQuery.FindContentAsync(Context, name, id); var content = await contentQuery.FindContentAsync(Context, name, id);
var response = ContentDto.FromContent(Context, content, this); var response = ContentDto.FromContent(Context, content, Resources);
return Ok(response); return Ok(response);
} }
@ -274,7 +274,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
var content = await contentQuery.FindContentAsync(Context, name, id, version); var content = await contentQuery.FindContentAsync(Context, name, id, version);
var response = ContentDto.FromContent(Context, content, this); var response = ContentDto.FromContent(Context, content, Resources);
return Ok(response.Data); return Ok(response.Data);
} }
@ -301,8 +301,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false)
{ {
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish };
var response = await InvokeCommandAsync(command); var response = await InvokeCommandAsync(command);
@ -331,8 +329,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(5)] [ApiCosts(5)]
public async Task<IActionResult> PostContents(string app, string name, [FromBody] ImportContentsDto request) public async Task<IActionResult> PostContents(string app, string name, [FromBody] ImportContentsDto request)
{ {
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = request.ToCommand(); var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
@ -364,8 +360,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(5)] [ApiCosts(5)]
public async Task<IActionResult> BulkContents(string app, string name, [FromBody] BulkUpdateDto request) public async Task<IActionResult> BulkContents(string app, string name, [FromBody] BulkUpdateDto request)
{ {
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = request.ToCommand(); var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
@ -398,8 +392,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutContent(string app, string name, Guid id, [FromBody] NamedContentData request) public async Task<IActionResult> PutContent(string app, string name, Guid id, [FromBody] NamedContentData request)
{ {
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() }; var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() };
var response = await InvokeCommandAsync(command); var response = await InvokeCommandAsync(command);
@ -429,8 +421,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request) public async Task<IActionResult> PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request)
{ {
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new PatchContent { ContentId = id, Data = request.ToCleaned() }; var command = new PatchContent { ContentId = id, Data = request.ToCleaned() };
var response = await InvokeCommandAsync(command); var response = await InvokeCommandAsync(command);
@ -460,8 +450,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutContentStatus(string app, string name, Guid id, ChangeStatusDto request) public async Task<IActionResult> PutContentStatus(string app, string name, Guid id, ChangeStatusDto request)
{ {
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = request.ToCommand(id); var command = request.ToCommand(id);
var response = await InvokeCommandAsync(command); var response = await InvokeCommandAsync(command);
@ -489,8 +477,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> CreateDraft(string app, string name, Guid id) public async Task<IActionResult> CreateDraft(string app, string name, Guid id)
{ {
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new CreateContentDraft { ContentId = id }; var command = new CreateContentDraft { ContentId = id };
var response = await InvokeCommandAsync(command); var response = await InvokeCommandAsync(command);
@ -518,8 +504,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteVersion(string app, string name, Guid id) public async Task<IActionResult> DeleteVersion(string app, string name, Guid id)
{ {
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new DeleteContentDraft { ContentId = id }; var command = new DeleteContentDraft { ContentId = id };
var response = await InvokeCommandAsync(command); var response = await InvokeCommandAsync(command);
@ -546,8 +530,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, Guid id) public async Task<IActionResult> DeleteContent(string app, string name, Guid id)
{ {
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new DeleteContent { ContentId = id }; var command = new DeleteContent { ContentId = id };
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);
@ -560,7 +542,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<IEnrichedContentEntity>(); var result = context.Result<IEnrichedContentEntity>();
var response = ContentDto.FromContent(Context, result, this); var response = ContentDto.FromContent(Context, result, Resources);
return response; return response;
} }

37
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -16,7 +16,6 @@ using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Models namespace Squidex.Areas.Api.Controllers.Contents.Models
@ -106,7 +105,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// </summary> /// </summary>
public long Version { get; set; } public long Version { get; set; }
public static ContentDto FromContent(Context context, IEnrichedContentEntity content, ApiController controller) public static ContentDto FromContent(Context context, IEnrichedContentEntity content, Resources resources)
{ {
var response = SimpleMapper.Map(content, new ContentDto()); var response = SimpleMapper.Map(content, new ContentDto());
@ -134,60 +133,62 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
SimpleMapper.Map(content.ScheduleJob, response.ScheduleJob); SimpleMapper.Map(content.ScheduleJob, response.ScheduleJob);
} }
return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name); return response.CreateLinksAsync(content, resources, content.SchemaId.Name);
} }
private ContentDto CreateLinksAsync(IEnrichedContentEntity content, ApiController controller, string app, string schema) private ContentDto CreateLinksAsync(IEnrichedContentEntity content, Resources resources, string schema)
{ {
var app = resources.App;
var values = new { app, name = schema, id = Id }; var values = new { app, name = schema, id = Id };
AddSelfLink(controller.Url<ContentsController>(x => nameof(x.GetContent), values)); AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContent), values));
if (Version > 0) if (Version > 0)
{ {
var versioned = new { app, name = schema, id = Id, version = Version - 1 }; var versioned = new { app, name = schema, id = Id, version = Version - 1 };
AddGetLink("previous", controller.Url<ContentsController>(x => nameof(x.GetContentVersion), versioned)); AddGetLink("previous", resources.Url<ContentsController>(x => nameof(x.GetContentVersion), versioned));
} }
if (NewStatus.HasValue) if (NewStatus.HasValue)
{ {
if (controller.HasPermission(Permissions.AppContentsVersionDelete, app, schema)) if (resources.CanDeleteContentVersion(schema))
{ {
AddDeleteLink("draft/delete", controller.Url<ContentsController>(x => nameof(x.DeleteVersion), values)); AddDeleteLink("draft/delete", resources.Url<ContentsController>(x => nameof(x.DeleteVersion), values));
} }
} }
else if (Status == Status.Published) else if (Status == Status.Published)
{ {
if (controller.HasPermission(Permissions.AppContentsVersionCreate, app, schema)) if (resources.CanCreateContentVersion(schema))
{ {
AddPostLink("draft/create", controller.Url<ContentsController>(x => nameof(x.CreateDraft), values)); AddPostLink("draft/create", resources.Url<ContentsController>(x => nameof(x.CreateDraft), values));
} }
} }
if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema) && content.NextStatuses != null) if (content.NextStatuses != null && resources.CanUpdateContent(schema))
{ {
foreach (var next in content.NextStatuses) foreach (var next in content.NextStatuses)
{ {
AddPutLink($"status/{next.Status}", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values), next.Color); AddPutLink($"status/{next.Status}", resources.Url<ContentsController>(x => nameof(x.PutContentStatus), values), next.Color);
} }
} }
if (content.IsSingleton == false && controller.HasPermission(Permissions.AppContentsDelete, app, schema)) if (content.IsSingleton == false && resources.CanDeleteContent(schema))
{ {
AddDeleteLink("delete", controller.Url<ContentsController>(x => nameof(x.DeleteContent), values)); AddDeleteLink("delete", resources.Url<ContentsController>(x => nameof(x.DeleteContent), values));
} }
if (content.CanUpdate) if (content.CanUpdate)
{ {
if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema)) if (resources.CanUpdateContent(schema))
{ {
AddPutLink("update", controller.Url<ContentsController>(x => nameof(x.PutContent), values)); AddPutLink("update", resources.Url<ContentsController>(x => nameof(x.PutContent), values));
} }
if (controller.HasPermission(Permissions.AppContentsUpdatePartial, app, schema)) if (resources.CanUpdateContentPartial(schema))
{ {
AddPatchLink("patch", controller.Url<ContentsController>(x => nameof(x.PatchContent), values)); AddPatchLink("patch", resources.Url<ContentsController>(x => nameof(x.PatchContent), values));
} }
} }

19
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs

@ -12,7 +12,6 @@ using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Models namespace Squidex.Areas.Api.Controllers.Contents.Models
@ -36,20 +35,20 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
[Required] [Required]
public StatusInfoDto[] Statuses { get; set; } public StatusInfoDto[] Statuses { get; set; }
public static async Task<ContentsDto> FromContentsAsync(IResultList<IEnrichedContentEntity> contents, Context context, ApiController controller, public static async Task<ContentsDto> FromContentsAsync(IResultList<IEnrichedContentEntity> contents, Context context, Resources resources,
ISchemaEntity? schema, IContentWorkflow workflow) ISchemaEntity? schema, IContentWorkflow workflow)
{ {
var result = new ContentsDto var result = new ContentsDto
{ {
Total = contents.Total, Total = contents.Total,
Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray() Items = contents.Select(x => ContentDto.FromContent(context, x, resources)).ToArray()
}; };
if (schema != null) if (schema != null)
{ {
await result.AssignStatusesAsync(workflow, schema); await result.AssignStatusesAsync(workflow, schema);
result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); result.CreateLinks(resources, schema.SchemaDef.Name);
} }
return result; return result;
@ -62,17 +61,17 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray(); Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray();
} }
private void CreateLinks(ApiController controller, string app, string schema) private void CreateLinks(Resources resources, string schema)
{ {
var values = new { app, name = schema }; var values = new { app = resources.App, name = schema };
AddSelfLink(controller.Url<ContentsController>(x => nameof(x.GetContents), values)); AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContents), values));
if (controller.HasPermission(Permissions.AppContentsCreate, app, schema)) if (resources.CanCreateContent(schema))
{ {
AddPostLink("create", controller.Url<ContentsController>(x => nameof(x.PostContent), values)); AddPostLink("create", resources.Url<ContentsController>(x => nameof(x.PostContent), values));
AddPostLink("create/publish", controller.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true"); AddPostLink("create/publish", resources.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true");
} }
} }
} }

8
backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs

@ -35,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
{ {
var eventConsumers = await GetGrain().GetConsumersAsync(); var eventConsumers = await GetGrain().GetConsumersAsync();
var response = EventConsumersDto.FromResults(eventConsumers.Value, this); var response = EventConsumersDto.FromResults(eventConsumers.Value, Resources);
return Ok(response); return Ok(response);
} }
@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
{ {
var eventConsumer = await GetGrain().StartAsync(name); var eventConsumer = await GetGrain().StartAsync(name);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, this); var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, Resources);
return Ok(response); return Ok(response);
} }
@ -61,7 +61,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
{ {
var eventConsumer = await GetGrain().StopAsync(name); var eventConsumer = await GetGrain().StopAsync(name);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, this); var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, Resources);
return Ok(response); return Ok(response);
} }
@ -74,7 +74,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
{ {
var eventConsumer = await GetGrain().ResetAsync(name); var eventConsumer = await GetGrain().ResetAsync(name);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, this); var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, Resources);
return Ok(response); return Ok(response);
} }

15
backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs

@ -7,7 +7,6 @@
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.EventConsumers.Models namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
@ -24,31 +23,31 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
public string? Position { get; set; } public string? Position { get; set; }
public static EventConsumerDto FromEventConsumerInfo(EventConsumerInfo eventConsumerInfo, ApiController controller) public static EventConsumerDto FromEventConsumerInfo(EventConsumerInfo eventConsumerInfo, Resources resources)
{ {
var result = SimpleMapper.Map(eventConsumerInfo, new EventConsumerDto()); var result = SimpleMapper.Map(eventConsumerInfo, new EventConsumerDto());
return result.CreateLinks(controller); return result.CreateLinks(resources);
} }
private EventConsumerDto CreateLinks(ApiController controller) private EventConsumerDto CreateLinks(Resources resources)
{ {
if (controller.HasPermission(Permissions.AdminEventsManage)) if (resources.CanManageEvents)
{ {
var values = new { name = Name }; var values = new { name = Name };
if (!IsResetting) if (!IsResetting)
{ {
AddPutLink("reset", controller.Url<EventConsumersController>(x => nameof(x.ResetEventConsumer), values)); AddPutLink("reset", resources.Url<EventConsumersController>(x => nameof(x.ResetEventConsumer), values));
} }
if (IsStopped) if (IsStopped)
{ {
AddPutLink("start", controller.Url<EventConsumersController>(x => nameof(x.StartEventConsumer), values)); AddPutLink("start", resources.Url<EventConsumersController>(x => nameof(x.StartEventConsumer), values));
} }
else else
{ {
AddPutLink("stop", controller.Url<EventConsumersController>(x => nameof(x.StopEventConsumer), values)); AddPutLink("stop", resources.Url<EventConsumersController>(x => nameof(x.StopEventConsumer), values));
} }
} }

10
backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs

@ -19,19 +19,19 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
/// </summary> /// </summary>
public EventConsumerDto[] Items { get; set; } public EventConsumerDto[] Items { get; set; }
public static EventConsumersDto FromResults(IEnumerable<EventConsumerInfo> items, ApiController controller) public static EventConsumersDto FromResults(IEnumerable<EventConsumerInfo> items, Resources resources)
{ {
var result = new EventConsumersDto var result = new EventConsumersDto
{ {
Items = items.Select(x => EventConsumerDto.FromEventConsumerInfo(x, controller)).ToArray() Items = items.Select(x => EventConsumerDto.FromEventConsumerInfo(x, resources)).ToArray()
}; };
return result.CreateLinks(controller); return result.CreateLinks(resources);
} }
private EventConsumersDto CreateLinks(ApiController controller) private EventConsumersDto CreateLinks(Resources resources)
{ {
AddSelfLink(controller.Url<EventConsumersController>(c => nameof(c.GetEventConsumers))); AddSelfLink(resources.Url<EventConsumersController>(c => nameof(c.GetEventConsumers)));
return this; return this;
} }

34
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs

@ -14,7 +14,6 @@ using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models namespace Squidex.Areas.Api.Controllers.Rules.Models
@ -91,7 +90,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
/// </summary> /// </summary>
public Instant? LastExecuted { get; set; } public Instant? LastExecuted { get; set; }
public static RuleDto FromRule(IEnrichedRuleEntity rule, Guid? runningRuleId, ApiController controller, string app) public static RuleDto FromRule(IEnrichedRuleEntity rule, Guid? runningRuleId, Resources resources)
{ {
var result = new RuleDto(); var result = new RuleDto();
@ -103,48 +102,45 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
result.Trigger = RuleTriggerDtoFactory.Create(rule.RuleDef.Trigger); result.Trigger = RuleTriggerDtoFactory.Create(rule.RuleDef.Trigger);
} }
return result.CreateLinks(controller, runningRuleId, app); return result.CreateLinks(resources, runningRuleId);
} }
private RuleDto CreateLinks(ApiController controller, Guid? runningRuleId, string app) private RuleDto CreateLinks(Resources resources, Guid? runningRuleId)
{ {
var values = new { app, id = Id }; var values = new { app = resources.App, id = Id };
if (controller.HasPermission(Permissions.AppRulesDisable, app)) if (resources.CanDisableRule)
{ {
if (IsEnabled) if (IsEnabled)
{ {
AddPutLink("disable", controller.Url<RulesController>(x => nameof(x.DisableRule), values)); AddPutLink("disable", resources.Url<RulesController>(x => nameof(x.DisableRule), values));
} }
else else
{ {
AddPutLink("enable", controller.Url<RulesController>(x => nameof(x.EnableRule), values)); AddPutLink("enable", resources.Url<RulesController>(x => nameof(x.EnableRule), values));
} }
} }
if (controller.HasPermission(Permissions.AppRulesUpdate)) if (resources.CanUpdateRule)
{ {
AddPutLink("update", controller.Url<RulesController>(x => nameof(x.PutRule), values)); AddPutLink("update", resources.Url<RulesController>(x => nameof(x.PutRule), values));
} }
if (controller.HasPermission(Permissions.AppRulesEvents)) if (resources.CanReadRuleEvents)
{ {
AddPutLink("trigger", controller.Url<RulesController>(x => nameof(x.TriggerRule), values)); AddPutLink("trigger", resources.Url<RulesController>(x => nameof(x.TriggerRule), values));
if (runningRuleId == null) if (runningRuleId == null)
{ {
AddPutLink("run", controller.Url<RulesController>(x => nameof(x.PutRuleRun), values)); AddPutLink("run", resources.Url<RulesController>(x => nameof(x.PutRuleRun), values));
} }
}
if (controller.HasPermission(Permissions.AppRulesDelete)) AddGetLink("logs", resources.Url<RulesController>(x => nameof(x.GetEvents), values));
{
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteRule), values));
} }
if (controller.HasPermission(Permissions.AppRulesEvents)) if (resources.CanDeleteRule)
{ {
AddGetLink("logs", controller.Url<RulesController>(x => nameof(x.GetEvents), values)); AddDeleteLink("delete", resources.Url<RulesController>(x => nameof(x.DeleteRule), values));
} }
return this; return this;

23
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models namespace Squidex.Areas.Api.Controllers.Rules.Models
@ -28,36 +27,36 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
/// </summary> /// </summary>
public Guid? RunningRuleId { get; set; } public Guid? RunningRuleId { get; set; }
public static RulesDto FromRules(IEnumerable<IEnrichedRuleEntity> items, Guid? runningRuleId, ApiController controller, string app) public static RulesDto FromRules(IEnumerable<IEnrichedRuleEntity> items, Guid? runningRuleId, Resources resources)
{ {
var result = new RulesDto var result = new RulesDto
{ {
Items = items.Select(x => RuleDto.FromRule(x, runningRuleId, controller, app)).ToArray() Items = items.Select(x => RuleDto.FromRule(x, runningRuleId, resources)).ToArray()
}; };
result.RunningRuleId = runningRuleId; result.RunningRuleId = runningRuleId;
return result.CreateLinks(controller, runningRuleId, app); return result.CreateLinks(resources, runningRuleId);
} }
private RulesDto CreateLinks(ApiController controller, Guid? runningRuleId, string app) private RulesDto CreateLinks(Resources resources, Guid? runningRuleId)
{ {
var values = new { app }; var values = new { app = resources.App };
AddSelfLink(controller.Url<RulesController>(x => nameof(x.GetRules), values)); AddSelfLink(resources.Url<RulesController>(x => nameof(x.GetRules), values));
if (controller.HasPermission(Permissions.AppRulesCreate, app)) if (resources.CanCreateRule)
{ {
AddPostLink("create", controller.Url<RulesController>(x => nameof(x.PostRule), values)); AddPostLink("create", resources.Url<RulesController>(x => nameof(x.PostRule), values));
} }
if (controller.HasPermission(Permissions.AppRulesEvents, app)) if (resources.CanReadRuleEvents)
{ {
AddGetLink("events", controller.Url<RulesController>(x => nameof(x.GetEvents), values)); AddGetLink("events", resources.Url<RulesController>(x => nameof(x.GetEvents), values));
if (runningRuleId != null) if (runningRuleId != null)
{ {
AddDeleteLink("run/cancel", controller.Url<RulesController>(x => nameof(x.DeleteRuleRun), values)); AddDeleteLink("run/cancel", resources.Url<RulesController>(x => nameof(x.DeleteRuleRun), values));
} }
} }

4
backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -95,7 +95,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
return RulesDto.FromRules(rules, runningRuleId, this, app); return RulesDto.FromRules(rules, runningRuleId, Resources);
}); });
return Ok(response); return Ok(response);
@ -363,7 +363,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(Context.App.Id); var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(Context.App.Id);
var result = context.Result<IEnrichedRuleEntity>(); var result = context.Result<IEnrichedRuleEntity>();
var response = RuleDto.FromRule(result, runningRuleId, this, app); var response = RuleDto.FromRule(result, runningRuleId, Resources);
return response; return response;
} }

29
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs

@ -102,53 +102,56 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
return result; return result;
} }
public void CreateLinks(ApiController controller, string app, string schema, bool allowUpdate) public void CreateLinks(Resources resources, string schema, bool allowUpdate)
{ {
allowUpdate = allowUpdate && !IsLocked; allowUpdate = allowUpdate && !IsLocked;
if (allowUpdate) if (allowUpdate)
{ {
var values = new { app, name = schema, id = FieldId }; var values = new { app = resources.App, name = schema, id = FieldId };
AddPutLink("update", controller.Url<SchemaFieldsController>(x => nameof(x.PutField), values)); AddPutLink("update", resources.Url<SchemaFieldsController>(x => nameof(x.PutField), values));
if (IsHidden) if (IsHidden)
{ {
AddPutLink("show", controller.Url<SchemaFieldsController>(x => nameof(x.ShowField), values)); AddPutLink("show", resources.Url<SchemaFieldsController>(x => nameof(x.ShowField), values));
} }
else else
{ {
AddPutLink("hide", controller.Url<SchemaFieldsController>(x => nameof(x.HideField), values)); AddPutLink("hide", resources.Url<SchemaFieldsController>(x => nameof(x.HideField), values));
} }
if (IsDisabled) if (IsDisabled)
{ {
AddPutLink("enable", controller.Url<SchemaFieldsController>(x => nameof(x.EnableField), values)); AddPutLink("enable", resources.Url<SchemaFieldsController>(x => nameof(x.EnableField), values));
} }
else else
{ {
AddPutLink("disable", controller.Url<SchemaFieldsController>(x => nameof(x.DisableField), values)); AddPutLink("disable", resources.Url<SchemaFieldsController>(x => nameof(x.DisableField), values));
} }
if (Properties is ArrayFieldPropertiesDto) if (Properties is ArrayFieldPropertiesDto)
{ {
var parentValues = new { app, name = schema, parentId = FieldId }; var parentValues = new { app = resources.App, name = schema, parentId = FieldId };
AddPostLink("fields/add", controller.Url<SchemaFieldsController>(x => nameof(x.PostNestedField), parentValues)); AddPostLink("fields/add", resources.Url<SchemaFieldsController>(x => nameof(x.PostNestedField), parentValues));
AddPutLink("fields/order", controller.Url<SchemaFieldsController>(x => nameof(x.PutNestedFieldOrdering), parentValues)); AddPutLink("fields/order", resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedFieldOrdering), parentValues));
} }
AddPutLink("lock", controller.Url<SchemaFieldsController>(x => nameof(x.LockField), values)); if (!IsLocked)
{
AddPutLink("lock", resources.Url<SchemaFieldsController>(x => nameof(x.LockField), values));
}
AddDeleteLink("delete", controller.Url<SchemaFieldsController>(x => nameof(x.DeleteField), values)); AddDeleteLink("delete", resources.Url<SchemaFieldsController>(x => nameof(x.DeleteField), values));
} }
if (Nested != null) if (Nested != null)
{ {
foreach (var nested in Nested) foreach (var nested in Nested)
{ {
nested.CreateLinks(controller, app, schema, FieldId, allowUpdate); nested.CreateLinks(resources, schema, FieldId, allowUpdate);
} }
} }
} }

21
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs

@ -45,37 +45,40 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
[Required] [Required]
public FieldPropertiesDto Properties { get; set; } public FieldPropertiesDto Properties { get; set; }
public void CreateLinks(ApiController controller, string app, string schema, long parentId, bool allowUpdate) public void CreateLinks(Resources resources, string schema, long parentId, bool allowUpdate)
{ {
allowUpdate = allowUpdate && !IsLocked; allowUpdate = allowUpdate && !IsLocked;
if (allowUpdate) if (allowUpdate)
{ {
var values = new { app, name = schema, parentId, id = FieldId }; var values = new { app = resources.App, name = schema, parentId, id = FieldId };
AddPutLink("update", controller.Url<SchemaFieldsController>(x => nameof(x.PutNestedField), values)); AddPutLink("update", resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedField), values));
if (IsHidden) if (IsHidden)
{ {
AddPutLink("show", controller.Url<SchemaFieldsController>(x => nameof(x.ShowNestedField), values)); AddPutLink("show", resources.Url<SchemaFieldsController>(x => nameof(x.ShowNestedField), values));
} }
else else
{ {
AddPutLink("hide", controller.Url<SchemaFieldsController>(x => nameof(x.HideNestedField), values)); AddPutLink("hide", resources.Url<SchemaFieldsController>(x => nameof(x.HideNestedField), values));
} }
if (IsDisabled) if (IsDisabled)
{ {
AddPutLink("enable", controller.Url<SchemaFieldsController>(x => nameof(x.EnableNestedField), values)); AddPutLink("enable", resources.Url<SchemaFieldsController>(x => nameof(x.EnableNestedField), values));
} }
else else
{ {
AddPutLink("disable", controller.Url<SchemaFieldsController>(x => nameof(x.DisableNestedField), values)); AddPutLink("disable", resources.Url<SchemaFieldsController>(x => nameof(x.DisableNestedField), values));
} }
AddPutLink("lock", controller.Url<SchemaFieldsController>(x => nameof(x.LockNestedField), values)); if (!IsLocked)
{
AddPutLink("lock", resources.Url<SchemaFieldsController>(x => nameof(x.LockNestedField), values));
}
AddDeleteLink("delete", controller.Url<SchemaFieldsController>(x => nameof(x.DeleteNestedField), values)); AddDeleteLink("delete", resources.Url<SchemaFieldsController>(x => nameof(x.DeleteNestedField), values));
} }
} }
} }

14
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs

@ -10,7 +10,6 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models namespace Squidex.Areas.Api.Controllers.Schemas.Models
@ -49,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
[Required] [Required]
public List<FieldDto> Fields { get; set; } public List<FieldDto> Fields { get; set; }
public static SchemaDetailsDto FromSchemaWithDetails(ISchemaEntity schema, ApiController controller, string app) public static SchemaDetailsDto FromSchemaWithDetails(ISchemaEntity schema, Resources resources)
{ {
var result = new SchemaDetailsDto(); var result = new SchemaDetailsDto();
@ -59,6 +58,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
SimpleMapper.Map(schema.SchemaDef.Properties, result.Properties); SimpleMapper.Map(schema.SchemaDef.Properties, result.Properties);
result.FieldsInLists = schema.SchemaDef.FieldsInLists.ToList(); result.FieldsInLists = schema.SchemaDef.FieldsInLists.ToList();
result.FieldsInReferences = schema.SchemaDef.FieldsInReferences.ToList(); result.FieldsInReferences = schema.SchemaDef.FieldsInReferences.ToList();
if (schema.SchemaDef.PreviewUrls.Count > 0) if (schema.SchemaDef.PreviewUrls.Count > 0)
@ -73,22 +73,22 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
result.Fields.Add(FieldDto.FromField(field)); result.Fields.Add(FieldDto.FromField(field));
} }
result.CreateLinks(controller, app); result.CreateLinks(resources);
return result; return result;
} }
protected override void CreateLinks(ApiController controller, string app) protected override void CreateLinks(Resources resources)
{ {
base.CreateLinks(controller, app); base.CreateLinks(resources);
var allowUpdate = controller.HasPermission(Permissions.AppSchemasUpdate, app, Name); var allowUpdate = resources.CanUpdateSchema(Name);
if (Fields != null) if (Fields != null)
{ {
foreach (var nested in Fields) foreach (var nested in Fields)
{ {
nested.CreateLinks(controller, app, Name, allowUpdate); nested.CreateLinks(resources, Name, allowUpdate);
} }
} }
} }

51
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs

@ -12,7 +12,6 @@ using Squidex.Areas.Api.Controllers.Contents;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models namespace Squidex.Areas.Api.Controllers.Schemas.Models
@ -79,7 +78,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary> /// </summary>
public long Version { get; set; } public long Version { get; set; }
public static SchemaDto FromSchema(ISchemaEntity schema, ApiController controller, string app) public static SchemaDto FromSchema(ISchemaEntity schema, Resources controller)
{ {
var result = new SchemaDto(); var result = new SchemaDto();
@ -87,64 +86,64 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
SimpleMapper.Map(schema.SchemaDef, result); SimpleMapper.Map(schema.SchemaDef, result);
SimpleMapper.Map(schema.SchemaDef.Properties, result.Properties); SimpleMapper.Map(schema.SchemaDef.Properties, result.Properties);
result.CreateLinks(controller, app); result.CreateLinks(controller);
return result; return result;
} }
protected virtual void CreateLinks(ApiController controller, string app) protected virtual void CreateLinks(Resources resources)
{ {
var values = new { app, name = Name }; var values = new { app = resources.App, name = Name };
var allowUpdate = controller.HasPermission(Permissions.AppSchemasUpdate, app, Name); var allowUpdate = resources.CanUpdateSchema(Name);
AddSelfLink(controller.Url<SchemasController>(x => nameof(x.GetSchema), values)); AddSelfLink(resources.Url<SchemasController>(x => nameof(x.GetSchema), values));
if (controller.HasPermission(Permissions.AppContentsRead, app, Name)) if (resources.CanReadContent(Name))
{ {
AddGetLink("contents", controller.Url<ContentsController>(x => nameof(x.GetContents), values)); AddGetLink("contents", resources.Url<ContentsController>(x => nameof(x.GetContents), values));
} }
if (controller.HasPermission(Permissions.AppContentsCreate, app, Name)) if (resources.CanCreateContent(Name))
{ {
AddPostLink("contents/create", controller.Url<ContentsController>(x => nameof(x.PostContent), values)); AddPostLink("contents/create", resources.Url<ContentsController>(x => nameof(x.PostContent), values));
AddPostLink("contents/create/publish", controller.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true"); AddPostLink("contents/create/publish", resources.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true");
} }
if (controller.HasPermission(Permissions.AppSchemasPublish, app, Name)) if (resources.CanPublishSchema(Name))
{ {
if (IsPublished) if (IsPublished)
{ {
AddPutLink("unpublish", controller.Url<SchemasController>(x => nameof(x.UnpublishSchema), values)); AddPutLink("unpublish", resources.Url<SchemasController>(x => nameof(x.UnpublishSchema), values));
} }
else else
{ {
AddPutLink("publish", controller.Url<SchemasController>(x => nameof(x.PublishSchema), values)); AddPutLink("publish", resources.Url<SchemasController>(x => nameof(x.PublishSchema), values));
} }
} }
if (allowUpdate) if (allowUpdate)
{ {
AddPostLink("fields/add", controller.Url<SchemaFieldsController>(x => nameof(x.PostField), values)); AddPostLink("fields/add", resources.Url<SchemaFieldsController>(x => nameof(x.PostField), values));
AddPutLink("fields/order", controller.Url<SchemaFieldsController>(x => nameof(x.PutSchemaFieldOrdering), values)); AddPutLink("fields/ui", resources.Url<SchemaFieldsController>(x => nameof(x.PutSchemaUIFields), values));
AddPutLink("fields/ui", controller.Url<SchemaFieldsController>(x => nameof(x.PutSchemaUIFields), values)); AddPutLink("fields/order", resources.Url<SchemaFieldsController>(x => nameof(x.PutSchemaFieldOrdering), values));
AddPutLink("update", controller.Url<SchemasController>(x => nameof(x.PutSchema), values)); AddPutLink("update", resources.Url<SchemasController>(x => nameof(x.PutSchema), values));
AddPutLink("update/category", controller.Url<SchemasController>(x => nameof(x.PutCategory), values)); AddPutLink("update/sync", resources.Url<SchemasController>(x => nameof(x.PutSchemaSync), values));
AddPutLink("update/sync", controller.Url<SchemasController>(x => nameof(x.PutSchemaSync), values)); AddPutLink("update/urls", resources.Url<SchemasController>(x => nameof(x.PutPreviewUrls), values));
AddPutLink("update/urls", controller.Url<SchemasController>(x => nameof(x.PutPreviewUrls), values)); AddPutLink("update/category", resources.Url<SchemasController>(x => nameof(x.PutCategory), values));
} }
if (controller.HasPermission(Permissions.AppSchemasScripts, app, Name)) if (resources.CanUpdateSchemaScripts(Name))
{ {
AddPutLink("update/scripts", controller.Url<SchemasController>(x => nameof(x.PutScripts), values)); AddPutLink("update/scripts", resources.Url<SchemasController>(x => nameof(x.PutScripts), values));
} }
if (controller.HasPermission(Permissions.AppSchemasDelete, app, Name)) if (resources.CanDeleteSchema(Name))
{ {
AddDeleteLink("delete", controller.Url<SchemasController>(x => nameof(x.DeleteSchema), values)); AddDeleteLink("delete", resources.Url<SchemasController>(x => nameof(x.DeleteSchema), values));
} }
} }
} }

17
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs

@ -8,7 +8,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models namespace Squidex.Areas.Api.Controllers.Schemas.Models
@ -20,25 +19,25 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary> /// </summary>
public SchemaDto[] Items { get; set; } public SchemaDto[] Items { get; set; }
public static SchemasDto FromSchemas(IList<ISchemaEntity> schemas, ApiController controller, string app) public static SchemasDto FromSchemas(IList<ISchemaEntity> schemas, Resources resources)
{ {
var result = new SchemasDto var result = new SchemasDto
{ {
Items = schemas.Select(x => SchemaDto.FromSchema(x, controller, app)).ToArray() Items = schemas.Select(x => SchemaDto.FromSchema(x, resources)).ToArray()
}; };
return result.CreateLinks(controller, app); return result.CreateLinks(resources);
} }
private SchemasDto CreateLinks(ApiController controller, string app) private SchemasDto CreateLinks(Resources resources)
{ {
var values = new { app }; var values = new { app = resources.App };
AddSelfLink(controller.Url<SchemasController>(x => nameof(x.GetSchemas), values)); AddSelfLink(resources.Url<SchemasController>(x => nameof(x.GetSchemas), values));
if (controller.HasPermission(Permissions.AppSchemasCreate, app)) if (resources.CanCreateSchema)
{ {
AddPostLink("create", controller.Url<SchemasController>(x => nameof(x.PostSchema), values)); AddPostLink("create", resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
} }
return this; return this;

42
backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = request.ToCommand(); var command = request.ToCommand();
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, name }, response); return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, name }, response);
} }
@ -75,7 +75,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = request.ToCommand(parentId); var command = request.ToCommand(parentId);
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, name }, response); return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, name }, response);
} }
@ -100,7 +100,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = request.ToCommand(); var command = request.ToCommand();
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -125,7 +125,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = request.ToCommand(); var command = request.ToCommand();
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -151,7 +151,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = request.ToCommand(parentId); var command = request.ToCommand(parentId);
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -177,7 +177,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = request.ToCommand(id); var command = request.ToCommand(id);
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -204,7 +204,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = request.ToCommand(id, parentId); var command = request.ToCommand(id, parentId);
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -231,7 +231,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new LockField { FieldId = id }; var command = new LockField { FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -259,7 +259,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new LockField { ParentFieldId = parentId, FieldId = id }; var command = new LockField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -286,7 +286,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new HideField { FieldId = id }; var command = new HideField { FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -314,7 +314,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new HideField { ParentFieldId = parentId, FieldId = id }; var command = new HideField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -341,7 +341,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new ShowField { FieldId = id }; var command = new ShowField { FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -369,7 +369,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new ShowField { ParentFieldId = parentId, FieldId = id }; var command = new ShowField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -396,7 +396,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new EnableField { FieldId = id }; var command = new EnableField { FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -424,7 +424,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new EnableField { ParentFieldId = parentId, FieldId = id }; var command = new EnableField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -451,7 +451,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new DisableField { FieldId = id }; var command = new DisableField { FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -479,7 +479,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new DisableField { ParentFieldId = parentId, FieldId = id }; var command = new DisableField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -504,7 +504,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new DeleteField { FieldId = id }; var command = new DeleteField { FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -530,17 +530,17 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var command = new DeleteField { ParentFieldId = parentId, FieldId = id }; var command = new DeleteField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
private async Task<SchemaDetailsDto> InvokeCommandAsync(string app, ICommand command) private async Task<SchemaDetailsDto> InvokeCommandAsync(ICommand command)
{ {
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<ISchemaEntity>(); var result = context.Result<ISchemaEntity>();
var response = SchemaDetailsDto.FromSchemaWithDetails(result, this, app); var response = SchemaDetailsDto.FromSchemaWithDetails(result, Resources);
return response; return response;
} }

6
backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -52,7 +52,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
return SchemasDto.FromSchemas(schemas, this, app); return SchemasDto.FromSchemas(schemas, Resources);
}); });
Response.Headers[HeaderNames.ETag] = schemas.ToEtag(); Response.Headers[HeaderNames.ETag] = schemas.ToEtag();
@ -94,7 +94,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {
return SchemaDetailsDto.FromSchemaWithDetails(schema, this, app); return SchemaDetailsDto.FromSchemaWithDetails(schema, Resources);
}); });
Response.Headers[HeaderNames.ETag] = schema.ToEtag(); Response.Headers[HeaderNames.ETag] = schema.ToEtag();
@ -320,7 +320,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<ISchemaEntity>(); var result = context.Result<ISchemaEntity>();
var response = SchemaDetailsDto.FromSchemaWithDetails(result, this, app); var response = SchemaDetailsDto.FromSchemaWithDetails(result, Resources);
return response; return response;
} }

21
backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs

@ -9,40 +9,39 @@ using Squidex.Areas.Api.Controllers.Backups;
using Squidex.Areas.Api.Controllers.EventConsumers; using Squidex.Areas.Api.Controllers.EventConsumers;
using Squidex.Areas.Api.Controllers.Languages; using Squidex.Areas.Api.Controllers.Languages;
using Squidex.Areas.Api.Controllers.Ping; using Squidex.Areas.Api.Controllers.Ping;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Users.Models namespace Squidex.Areas.Api.Controllers.Users.Models
{ {
public sealed class ResourcesDto : Resource public sealed class ResourcesDto : Resource
{ {
public static ResourcesDto FromController(ApiController controller) public static ResourcesDto FromResources(Resources resources)
{ {
var result = new ResourcesDto(); var result = new ResourcesDto();
result.AddGetLink("ping", controller.Url<PingController>(x => nameof(x.GetPing))); result.AddGetLink("ping", resources.Url<PingController>(x => nameof(x.GetPing)));
if (controller.HasPermission(Permissions.AdminEventsRead)) if (resources.CanReadEvents)
{ {
result.AddGetLink("admin/events", controller.Url<EventConsumersController>(x => nameof(x.GetEventConsumers))); result.AddGetLink("admin/events", resources.Url<EventConsumersController>(x => nameof(x.GetEventConsumers)));
} }
if (controller.HasPermission(Permissions.AdminRestore)) if (resources.CanRestoreBackup)
{ {
result.AddGetLink("admin/restore", controller.Url<RestoreController>(x => nameof(x.GetRestoreJob))); result.AddGetLink("admin/restore", resources.Url<RestoreController>(x => nameof(x.GetRestoreJob)));
} }
if (controller.HasPermission(Permissions.AdminUsersRead)) if (resources.CanReadUsers)
{ {
result.AddGetLink("admin/users", controller.Url<UserManagementController>(x => nameof(x.GetUsers))); result.AddGetLink("admin/users", resources.Url<UserManagementController>(x => nameof(x.GetUsers)));
} }
if (controller.HasPermission(Permissions.AdminOrleans)) if (resources.CanReadOrleans)
{ {
result.AddGetLink("admin/orleans", "/orleans"); result.AddGetLink("admin/orleans", "/orleans");
} }
result.AddGetLink("languages", controller.Url<LanguagesController>(x => nameof(x.GetLanguages))); result.AddGetLink("languages", resources.Url<LanguagesController>(x => nameof(x.GetLanguages)));
return result; return result;
} }

29
backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs

@ -10,7 +10,6 @@ using System.ComponentModel.DataAnnotations;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Users; using Squidex.Shared.Users;
using Squidex.Web; using Squidex.Web;
using AllPermissions = Squidex.Shared.Permissions;
namespace Squidex.Areas.Api.Controllers.Users.Models namespace Squidex.Areas.Api.Controllers.Users.Models
{ {
@ -46,48 +45,48 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
[Required] [Required]
public IEnumerable<string> Permissions { get; set; } public IEnumerable<string> Permissions { get; set; }
public static UserDto FromUser(IUser user, ApiController controller) public static UserDto FromUser(IUser user, Resources resources)
{ {
var userPermssions = user.Permissions().ToIds(); var userPermssions = user.Permissions().ToIds();
var userName = user.DisplayName()!; var userName = user.DisplayName()!;
var result = SimpleMapper.Map(user, new UserDto { DisplayName = userName, Permissions = userPermssions }); var result = SimpleMapper.Map(user, new UserDto { DisplayName = userName, Permissions = userPermssions });
return result.CreateLinks(controller); return result.CreateLinks(resources);
} }
private UserDto CreateLinks(ApiController controller) private UserDto CreateLinks(Resources resources)
{ {
var values = new { id = Id }; var values = new { id = Id };
if (controller is UserManagementController) if (resources.Controller is UserManagementController)
{ {
AddSelfLink(controller.Url<UserManagementController>(c => nameof(c.GetUser), values)); AddSelfLink(resources.Url<UserManagementController>(c => nameof(c.GetUser), values));
} }
else else
{ {
AddSelfLink(controller.Url<UsersController>(c => nameof(c.GetUser), values)); AddSelfLink(resources.Url<UsersController>(c => nameof(c.GetUser), values));
} }
if (!controller.IsUser(Id)) if (!resources.Controller.IsUser(Id))
{ {
if (controller.HasPermission(AllPermissions.AdminUsersLock) && !IsLocked) if (resources.CanLockUser && !IsLocked)
{ {
AddPutLink("lock", controller.Url<UserManagementController>(c => nameof(c.LockUser), values)); AddPutLink("lock", resources.Url<UserManagementController>(c => nameof(c.LockUser), values));
} }
if (controller.HasPermission(AllPermissions.AdminUsersUnlock) && IsLocked) if (resources.CanUnlockUser && IsLocked)
{ {
AddPutLink("unlock", controller.Url<UserManagementController>(c => nameof(c.UnlockUser), values)); AddPutLink("unlock", resources.Url<UserManagementController>(c => nameof(c.UnlockUser), values));
} }
} }
if (controller.HasPermission(AllPermissions.AdminUsersUpdate)) if (resources.CanUpdateUser)
{ {
AddPutLink("update", controller.Url<UserManagementController>(c => nameof(c.PutUser), values)); AddPutLink("update", resources.Url<UserManagementController>(c => nameof(c.PutUser), values));
} }
AddGetLink("picture", controller.Url<UsersController>(c => nameof(c.GetUserPicture), values)); AddGetLink("picture", resources.Url<UsersController>(c => nameof(c.GetUserPicture), values));
return this; return this;
} }

15
backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs

@ -9,7 +9,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Users.Models namespace Squidex.Areas.Api.Controllers.Users.Models
@ -27,24 +26,24 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
[Required] [Required]
public UserDto[] Items { get; set; } public UserDto[] Items { get; set; }
public static UsersDto FromResults(IEnumerable<UserWithClaims> items, long total, ApiController controller) public static UsersDto FromResults(IEnumerable<UserWithClaims> items, long total, Resources resources)
{ {
var result = new UsersDto var result = new UsersDto
{ {
Total = total, Total = total,
Items = items.Select(x => UserDto.FromUser(x, controller)).ToArray() Items = items.Select(x => UserDto.FromUser(x, resources)).ToArray()
}; };
return result.CreateLinks(controller); return result.CreateLinks(resources);
} }
private UsersDto CreateLinks(ApiController controller) private UsersDto CreateLinks(Resources context)
{ {
AddSelfLink(controller.Url<UserManagementController>(c => nameof(c.GetUsers))); AddSelfLink(context.Url<UserManagementController>(c => nameof(c.GetUsers)));
if (controller.HasPermission(Permissions.AdminUsersCreate)) if (context.CanCreateUser)
{ {
AddPostLink("create", controller.Url<UserManagementController>(c => nameof(c.PostUser))); AddPostLink("create", context.Url<UserManagementController>(c => nameof(c.PostUser)));
} }
return this; return this;

12
backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs

@ -41,7 +41,7 @@ namespace Squidex.Areas.Api.Controllers.Users
await Task.WhenAll(taskForItems, taskForCount); await Task.WhenAll(taskForItems, taskForCount);
var response = UsersDto.FromResults(taskForItems.Result, taskForCount.Result, this); var response = UsersDto.FromResults(taskForItems.Result, taskForCount.Result, Resources);
return Ok(response); return Ok(response);
} }
@ -59,7 +59,7 @@ namespace Squidex.Areas.Api.Controllers.Users
return NotFound(); return NotFound();
} }
var response = UserDto.FromUser(user, this); var response = UserDto.FromUser(user, Resources);
return Ok(response); return Ok(response);
} }
@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Users
{ {
var user = await userManager.CreateAsync(userFactory, request.ToValues()); var user = await userManager.CreateAsync(userFactory, request.ToValues());
var response = UserDto.FromUser(user, this); var response = UserDto.FromUser(user, Resources);
return Ok(response); return Ok(response);
} }
@ -85,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Users
{ {
var user = await userManager.UpdateAsync(id, request.ToValues()); var user = await userManager.UpdateAsync(id, request.ToValues());
var response = UserDto.FromUser(user, this); var response = UserDto.FromUser(user, Resources);
return Ok(response); return Ok(response);
} }
@ -103,7 +103,7 @@ namespace Squidex.Areas.Api.Controllers.Users
var user = await userManager.LockAsync(id); var user = await userManager.LockAsync(id);
var response = UserDto.FromUser(user, this); var response = UserDto.FromUser(user, Resources);
return Ok(response); return Ok(response);
} }
@ -121,7 +121,7 @@ namespace Squidex.Areas.Api.Controllers.Users
var user = await userManager.UnlockAsync(id); var user = await userManager.UnlockAsync(id);
var response = UserDto.FromUser(user, this); var response = UserDto.FromUser(user, Resources);
return Ok(response); return Ok(response);
} }

6
backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission] [ApiPermission]
public IActionResult GetUserResources() public IActionResult GetUserResources()
{ {
var response = ResourcesDto.FromController(this); var response = ResourcesDto.FromResources(Resources);
return Ok(response); return Ok(response);
} }
@ -97,7 +97,7 @@ namespace Squidex.Areas.Api.Controllers.Users
{ {
var users = await userResolver.QueryByEmailAsync(query); var users = await userResolver.QueryByEmailAsync(query);
var response = users.Where(x => !x.IsHidden()).Select(x => UserDto.FromUser(x, this)).ToArray(); var response = users.Where(x => !x.IsHidden()).Select(x => UserDto.FromUser(x, Resources)).ToArray();
return Ok(response); return Ok(response);
} }
@ -131,7 +131,7 @@ namespace Squidex.Areas.Api.Controllers.Users
if (entity != null) if (entity != null)
{ {
var response = UserDto.FromUser(entity, this); var response = UserDto.FromUser(entity, Resources);
return Ok(response); return Ok(response);
} }

3
backend/src/Squidex/Areas/Api/Startup.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Squidex.Areas.Api.Config;
using Squidex.Areas.Api.Config.OpenApi; using Squidex.Areas.Api.Config.OpenApi;
using Squidex.Web; using Squidex.Web;
@ -17,6 +18,8 @@ namespace Squidex.Areas.Api
{ {
app.Map(Constants.ApiPrefix, appApi => app.Map(Constants.ApiPrefix, appApi =>
{ {
appApi.UseMiddleware<IdentityServerPathMiddleware>();
appApi.UseRouting(); appApi.UseRouting();
appApi.UseAuthentication(); appApi.UseAuthentication();

32
backend/src/Squidex/Config/Authentication/IdentityServerServices.cs

@ -38,24 +38,24 @@ namespace Squidex.Config.Authentication
apiAuthorityUrl = urlsOptions.BuildUrl(Constants.IdentityServerPrefix); apiAuthorityUrl = urlsOptions.BuildUrl(Constants.IdentityServerPrefix);
} }
authBuilder.AddIdentityServerAuthentication(options => if (identityOptions.LocalApi)
{ {
options.Authority = apiAuthorityUrl; authBuilder.AddLocalApi(Constants.ApiSecurityScheme, options =>
options.ApiName = apiScope;
options.ApiSecret = null;
options.RequireHttpsMetadata = identityOptions.RequiresHttps;
options.SupportedTokens = SupportedTokens.Jwt;
var fromHeader = TokenRetrieval.FromAuthorizationHeader();
var fromQuery = TokenRetrieval.FromQueryString();
options.TokenRetriever = request =>
{ {
var result = fromHeader(request) ?? fromQuery(request); options.ExpectedScope = apiScope;
});
return result; }
}; else
}); {
authBuilder.AddIdentityServerAuthentication(Constants.ApiSecurityScheme, options =>
{
options.Authority = apiAuthorityUrl;
options.ApiName = apiScope;
options.ApiSecret = null;
options.RequireHttpsMetadata = identityOptions.RequiresHttps;
options.SupportedTokens = SupportedTokens.Jwt;
});
}
authBuilder.AddOpenIdConnect(options => authBuilder.AddOpenIdConnect(options =>
{ {

2
backend/src/Squidex/Config/MyIdentityOptions.cs

@ -55,6 +55,8 @@ namespace Squidex.Config
public bool AllowPasswordAuth { get; set; } public bool AllowPasswordAuth { get; set; }
public bool LocalApi { get; set; } = true;
public bool LockAutomatically { get; set; } public bool LockAutomatically { get; set; }
public bool NoConsent { get; set; } public bool NoConsent { get; set; }

4
backend/src/Squidex/Config/Web/WebServices.cs

@ -37,6 +37,9 @@ namespace Squidex.Config.Web
services.AddSingletonAs<AppResolver>() services.AddSingletonAs<AppResolver>()
.AsSelf(); .AsSelf();
services.AddSingletonAs<SchemaResolver>()
.AsSelf();
services.AddSingletonAs<RobotsTxtMiddleware>() services.AddSingletonAs<RobotsTxtMiddleware>()
.AsSelf(); .AsSelf();
@ -77,6 +80,7 @@ namespace Squidex.Config.Web
options.Filters.Add<CachingFilter>(); options.Filters.Add<CachingFilter>();
options.Filters.Add<DeferredActionFilter>(); options.Filters.Add<DeferredActionFilter>();
options.Filters.Add<AppResolver>(); options.Filters.Add<AppResolver>();
options.Filters.Add<SchemaResolver>();
options.Filters.Add<MeasureResultFilter>(); options.Filters.Add<MeasureResultFilter>();
}) })
.AddRazorRuntimeCompilation() .AddRazorRuntimeCompilation()

6
backend/src/Squidex/appsettings.json

@ -504,11 +504,15 @@
/* /*
* Set to true to show PII (Personally Identifiable Information) in the logs. * Set to true to show PII (Personally Identifiable Information) in the logs.
*/ */
"showPII": false, "showPII": true,
/* /*
* Enable password auth. Set this to false if you want to disable local login, leaving only 3rd party login options. * Enable password auth. Set this to false if you want to disable local login, leaving only 3rd party login options.
*/ */
"allowPasswordAuth": true, "allowPasswordAuth": true,
/*
* Uses local API authentication.
*/
"localApi": true,
/* /*
* Initial admin user. * Initial admin user.
*/ */

29
backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,8 +14,10 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Squidex.Infrastructure;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Squidex.Web.Pipeline;
using Xunit; using Xunit;
#pragma warning disable IDE0017 // Simplify object initialization #pragma warning disable IDE0017 // Simplify object initialization
@ -49,17 +52,17 @@ namespace Squidex.Web
} }
[Fact] [Fact]
public void Should_use_bearer_schemes() public void Should_use_custom_authorization_scheme()
{ {
var sut = new ApiPermissionAttribute(); var sut = new ApiPermissionAttribute();
Assert.Equal("Bearer", sut.AuthenticationSchemes); Assert.Equal(Constants.ApiSecurityScheme, sut.AuthenticationSchemes);
} }
[Fact] [Fact]
public async Task Should_call_next_when_user_has_correct_permission() public async Task Should_make_permission_check_with_app_feature()
{ {
actionExecutingContext.RouteData.Values["app"] = "my-app"; actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(NamedId.Of(Guid.NewGuid(), "my-app")));
user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app"));
@ -71,10 +74,26 @@ namespace Squidex.Web
Assert.True(isNextCalled); Assert.True(isNextCalled);
} }
[Fact]
public async Task Should_make_permission_check_with_schema_feature()
{
actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(NamedId.Of(Guid.NewGuid(), "my-app")));
actionExecutingContext.HttpContext.Features.Set<ISchemaFeature>(new SchemaFeature(NamedId.Of(Guid.NewGuid(), "my-schema")));
user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app.schemas.my-schema"));
var sut = new ApiPermissionAttribute(Permissions.AppSchemasUpdate);
await sut.OnActionExecutionAsync(actionExecutingContext, next);
Assert.Null(actionExecutingContext.Result);
Assert.True(isNextCalled);
}
[Fact] [Fact]
public async Task Should_return_forbidden_when_user_has_wrong_permission() public async Task Should_return_forbidden_when_user_has_wrong_permission()
{ {
actionExecutingContext.RouteData.Values["app"] = "my-app"; actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(NamedId.Of(Guid.NewGuid(), "my-app")));
user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app"));

151
backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs

@ -0,0 +1,151 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Xunit;
#pragma warning disable IDE0017 // Simplify object initialization
namespace Squidex.Web.Pipeline
{
public class SchemaResolverTests
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly HttpContext httpContext = new DefaultHttpContext();
private readonly ActionContext actionContext;
private readonly ActionExecutingContext actionExecutingContext;
private readonly ActionExecutionDelegate next;
private readonly ClaimsIdentity user = new ClaimsIdentity();
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly SchemaResolver sut;
private bool isNextCalled;
public SchemaResolverTests()
{
actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor
{
EndpointMetadata = new List<object>()
});
actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object>(), this);
actionExecutingContext.HttpContext = httpContext;
actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user);
next = () =>
{
isNextCalled = true;
return Task.FromResult<ActionExecutedContext?>(null);
};
sut = new SchemaResolver(appProvider);
}
[Fact]
public async Task Should_return_not_found_if_schema_not_found()
{
actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(appId));
actionContext.RouteData.Values["name"] = schemaId.Id.ToString();
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false))
.Returns(Task.FromResult<ISchemaEntity?>(null));
await sut.OnActionExecutionAsync(actionExecutingContext, next);
Assert.IsType<NotFoundResult>(actionExecutingContext.Result);
Assert.False(isNextCalled);
}
[Fact]
public async Task Should_resolve_schema_from_id()
{
actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(appId));
actionContext.RouteData.Values["name"] = schemaId.Id.ToString();
var schema = CreateSchema();
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false))
.Returns(schema);
await sut.OnActionExecutionAsync(actionExecutingContext, next);
Assert.Equal(schemaId, actionContext.HttpContext.Features.Get<ISchemaFeature>().SchemaId);
Assert.True(isNextCalled);
}
[Fact]
public async Task Should_resolve_schema_from_name()
{
actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(appId));
actionContext.RouteData.Values["name"] = schemaId.Name;
var schema = CreateSchema();
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name))
.Returns(schema);
await sut.OnActionExecutionAsync(actionExecutingContext, next);
Assert.Equal(schemaId, actionContext.HttpContext.Features.Get<ISchemaFeature>().SchemaId);
Assert.True(isNextCalled);
}
[Fact]
public async Task Should_do_nothing_if_app_feature_not_set()
{
actionExecutingContext.RouteData.Values["name"] = schemaId.Name;
await sut.OnActionExecutionAsync(actionExecutingContext, next);
Assert.True(isNextCalled);
A.CallTo(() => appProvider.GetAppAsync(A<string>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_do_nothing_if_parameter_not_set()
{
actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(appId));
actionExecutingContext.RouteData.Values.Remove("name");
await sut.OnActionExecutionAsync(actionExecutingContext, next);
Assert.True(isNextCalled);
A.CallTo(() => appProvider.GetAppAsync(A<string>._))
.MustNotHaveHappened();
}
private ISchemaEntity CreateSchema()
{
var schemaEntity = A.Fake<ISchemaEntity>();
A.CallTo(() => schemaEntity.SchemaDef)
.Returns(new Schema(schemaId.Name));
A.CallTo(() => schemaEntity.Id)
.Returns(schemaId.Id);
return schemaEntity;
}
}
}
Loading…
Cancel
Save