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. 22
      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 Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using AllPermissions = Squidex.Shared.Permissions;
using P = Squidex.Shared.Permissions;
namespace Squidex.Domain.Apps.Core.Apps
{
@ -59,12 +59,12 @@ namespace Squidex.Domain.Apps.Core.Apps
{
var result = new HashSet<Permission>
{
AllPermissions.ForApp(AllPermissions.AppCommon, app)
P.ForApp(P.AppCommon, app)
};
if (Permissions.Any())
{
var prefix = AllPermissions.ForApp(AllPermissions.App, app).Id;
var prefix = P.ForApp(P.App, app).Id;
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 AppRulesDelete = "squidex.apps.{app}.rules.delete";
public const string AppSchemas = "squidex.apps.{app}.schemas.{name}";
public const string AppSchemasCreate = "squidex.apps.{app}.schemas.{name}.create";
public const string AppSchemas = "squidex.apps.{app}.schemas";
public const string AppSchemasCreate = "squidex.apps.{app}.schemas.create";
public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update";
public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts";
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)]
public abstract class ApiController : Controller
{
private readonly Lazy<Resources> resources;
protected ICommandBus CommandBus { get; }
protected IAppEntity App
@ -38,6 +40,11 @@ namespace Squidex.Web
}
}
protected Resources Resources
{
get { return resources.Value; }
}
protected Context Context
{
get { return HttpContext.Context(); }
@ -53,6 +60,8 @@ namespace Squidex.Web
Guard.NotNull(commandBus);
CommandBus = commandBus;
resources = new Lazy<Resources>(() => new Resources(this));
}
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.Filters;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
namespace Squidex.Web
{
@ -25,7 +26,7 @@ namespace Squidex.Web
public ApiPermissionAttribute(params string[] ids)
{
AuthenticationSchemes = "Bearer";
AuthenticationSchemes = Constants.ApiSecurityScheme;
permissionIds = ids;
}
@ -40,16 +41,25 @@ namespace Squidex.Web
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;
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 ApiSecurityScheme = "identity-server";
public static readonly string OrleansClusterId = "squidex-v2";
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" />
</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="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</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)
{
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)
{
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)
{
return AppLanguagesDto.FromApp(result, this);
return AppLanguagesDto.FromApp(result, Resources);
}
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)
{
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)
{
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)
{
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()
{
var userOrClientId = HttpContext.User.UserOrClientId()!;
var userPermissions = HttpContext.Permissions();
var userPermissions = Resources.Permissions;
var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions);
var response = Deferred.Response(() =>
{
var userPermissions = HttpContext.Permissions();
return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)).ToArray();
return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, appPlansProvider, Resources)).ToArray();
});
Response.Headers[HeaderNames.ETag] = apps.ToEtag();
@ -107,9 +105,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
var response = Deferred.Response(() =>
{
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();
@ -299,10 +297,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
var context = await CommandBus.PublishAsync(command);
var userOrClientId = HttpContext.User.UserOrClientId()!;
var userPermissions = HttpContext.Permissions();
var result = context.Result<IAppEntity>();
var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this);
var response = AppDto.FromApp(result, userOrClientId, appPlansProvider, Resources);
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.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Web;
using AllPermissions = Squidex.Shared.Permissions;
using P = Squidex.Shared.Permissions;
#pragma warning disable RECS0033 // Convert 'if' to '||' expression
@ -91,31 +90,31 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
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());
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;
}
if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppContents, app.Name), permissions))
if (resources.Includes(P.ForApp(P.AppContents, app.Name), permissions))
{
result.CanAccessContent = true;
}
result.SetPlan(app, plans, controller, permissions);
result.SetImage(app, controller);
result.SetPlan(app, plans, resources, permissions);
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>();
@ -124,17 +123,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
permissions.AddRange(role.Permissions);
}
if (userPermissions != null)
{
permissions.AddRange(userPermissions.ToAppPermissions(app.Name));
}
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;
}
@ -142,100 +136,100 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
PlanName = plans.GetPlanForApp(app).Plan.Name;
}
private void SetImage(IAppEntity app, ApiController controller)
private void SetImage(IAppEntity app, Resources resources)
{
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 };
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;

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.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -59,20 +58,20 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
return result;
}
public AppLanguageDto WithLinks(ApiController controller, IAppEntity app)
public AppLanguageDto WithLinks(Resources resources, IAppEntity app)
{
var values = new { app = app.Name, language = Iso2Code };
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.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -21,7 +20,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
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;
@ -29,23 +28,23 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{
Items = config.Languages
.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)
.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;

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

@ -8,7 +8,6 @@
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -45,18 +44,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
return result;
}
public ClientDto WithLinks(ApiController controller, string app)
public ClientDto WithLinks(Resources resources, string app)
{
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;

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

@ -8,7 +8,6 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -21,7 +20,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
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;
@ -30,22 +29,22 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
Items =
app.Clients
.Select(x => ClientDto.FromClient(x.Key, x.Value))
.Select(x => x.WithLinks(controller, appName))
.Select(x => x.WithLinks(resources, appName))
.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 };
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;

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

@ -7,7 +7,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Squidex.Shared;
using Squidex.Shared.Users;
using Squidex.Web;
@ -62,18 +61,20 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
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 Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Plans;
using Squidex.Shared;
using Squidex.Shared.Users;
using Squidex.Web;
@ -36,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[JsonProperty("_meta")]
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());
@ -46,7 +45,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
app.Contributors
.Select(x => ContributorDto.FromIdAndRole(x.Key, x.Value))
.Select(x => x.WithUser(users))
.Select(x => x.WithLinks(controller, app.Name))
.Select(x => x.WithLinks(resources))
.OrderBy(x => x.ContributorName)
.ToArray()
};
@ -54,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
result.WithInvited(invited);
result.WithPlan(app, plans);
return result.CreateLinks(controller, app.Name);
return result.CreateLinks(resources, app.Name);
}
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 };
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;

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

@ -9,7 +9,6 @@ using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -38,25 +37,25 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
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 });
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;

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

@ -8,7 +8,6 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -21,25 +20,25 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
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
{
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;

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.Entities.Apps;
using Squidex.Web;
using AllPermissions = Squidex.Shared.Permissions;
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));
}
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 (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.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -21,32 +20,30 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
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
{
Items =
app.Roles.All
.Select(x => RoleDto.FromRole(x, app))
.Select(x => x.WithLinks(controller, appName))
.Select(x => x.WithLinks(resources))
.OrderBy(x => x.Name)
.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;

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

@ -11,7 +11,6 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -57,18 +56,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
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;

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

@ -10,7 +10,6 @@ using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -29,16 +28,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
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
{
Items =
app.Workflows
.Select(x => WorkflowDto.FromWorkflow(x.Key, x.Value))
.Select(x => x.WithLinks(controller, appName))
.Select(x => x.WithLinks(resources))
.ToArray()
};
@ -46,18 +43,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
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;

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

@ -20,7 +20,6 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log;
using Squidex.Shared;
using Squidex.Web;
#pragma warning disable 1573
@ -119,7 +118,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
return NotFound();
}
if (asset.IsProtected && !this.HasPermission(Permissions.AppAssetsRead))
if (asset.IsProtected && !Resources.CanReadEvents)
{
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(() =>
{
return AssetFoldersDto.FromAssets(assetFolders, this, app);
return AssetFoldersDto.FromAssets(assetFolders, Resources);
});
Response.Headers[HeaderNames.ETag] = assetFolders.ToEtag();
@ -162,7 +162,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
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(() =>
{
return AssetsDto.FromAssets(assets, this, app);
return AssetsDto.FromAssets(assets, Resources);
});
return Ok(response);
@ -129,7 +129,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var response = Deferred.Response(() =>
{
return AssetsDto.FromAssets(assets, this, app);
return AssetsDto.FromAssets(assets, Resources);
});
return Ok(response);
@ -160,7 +160,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var response = Deferred.Response(() =>
{
return AssetDto.FromAsset(asset, this, app);
return AssetDto.FromAsset(asset, Resources);
});
return Ok(response);
@ -304,11 +304,11 @@ namespace Squidex.Areas.Api.Controllers.Assets
if (context.PlainResult is AssetCreatedResult created)
{
return AssetDto.FromAsset(created.Asset, this, app, created.IsDuplicate);
return AssetDto.FromAsset(created.Asset, Resources, created.IsDuplicate);
}
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.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models
@ -158,7 +157,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
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());
@ -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 };
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;
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/slug", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Slug }));
response.AddGetLink("content", resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Id, more = response.Slug }));
response.AddGetLink("content/slug", resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Slug }));
}
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;

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

@ -9,7 +9,6 @@ using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models
@ -37,29 +36,29 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary>
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());
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;

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

@ -9,7 +9,6 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models
@ -27,26 +26,26 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[Required]
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
{
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;

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

@ -9,7 +9,6 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models
@ -27,29 +26,29 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[Required]
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
{
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;
}

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 response = BackupJobsDto.FromBackups(jobs, this, app);
var response = BackupJobsDto.FromBackups(jobs, Resources);
return Ok(response);
}

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

@ -9,7 +9,6 @@ using System;
using NodaTime;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Backups.Models
@ -46,23 +45,23 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models
/// </summary>
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());
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;
}

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.Linq;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Backups.Models
@ -22,25 +21,25 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models
[Required]
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
{
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;

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

@ -124,7 +124,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var response = Deferred.AsyncResponse(() =>
{
return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow);
return ContentsDto.FromContentsAsync(contents, Context, Resources, null, contentWorkflow);
});
return Ok(response);
@ -153,7 +153,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var response = Deferred.AsyncResponse(() =>
{
return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow);
return ContentsDto.FromContentsAsync(contents, Context, Resources, null, contentWorkflow);
});
return Ok(response);
@ -186,7 +186,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
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);
@ -218,7 +218,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
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);
@ -246,7 +246,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
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);
}
@ -274,7 +274,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
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);
}
@ -301,8 +301,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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 response = await InvokeCommandAsync(command);
@ -331,8 +329,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(5)]
public async Task<IActionResult> PostContents(string app, string name, [FromBody] ImportContentsDto request)
{
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command);
@ -364,8 +360,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(5)]
public async Task<IActionResult> BulkContents(string app, string name, [FromBody] BulkUpdateDto request)
{
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command);
@ -398,8 +392,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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 response = await InvokeCommandAsync(command);
@ -429,8 +421,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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 response = await InvokeCommandAsync(command);
@ -460,8 +450,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> PutContentStatus(string app, string name, Guid id, ChangeStatusDto request)
{
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = request.ToCommand(id);
var response = await InvokeCommandAsync(command);
@ -489,8 +477,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> CreateDraft(string app, string name, Guid id)
{
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new CreateContentDraft { ContentId = id };
var response = await InvokeCommandAsync(command);
@ -518,8 +504,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> DeleteVersion(string app, string name, Guid id)
{
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new DeleteContentDraft { ContentId = id };
var response = await InvokeCommandAsync(command);
@ -546,8 +530,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, Guid id)
{
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new DeleteContent { ContentId = id };
await CommandBus.PublishAsync(command);
@ -560,7 +542,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IEnrichedContentEntity>();
var response = ContentDto.FromContent(Context, result, this);
var response = ContentDto.FromContent(Context, result, Resources);
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.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Models
@ -106,7 +105,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// </summary>
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());
@ -134,60 +133,62 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
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 };
AddSelfLink(controller.Url<ContentsController>(x => nameof(x.GetContent), values));
AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContent), values));
if (Version > 0)
{
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 (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)
{
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)
{
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 (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.Schemas;
using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Models
@ -36,20 +35,20 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
[Required]
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)
{
var result = new ContentsDto
{
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)
{
await result.AssignStatusesAsync(workflow, schema);
result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name);
result.CreateLinks(resources, schema.SchemaDef.Name);
}
return result;
@ -62,17 +61,17 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
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 response = EventConsumersDto.FromResults(eventConsumers.Value, this);
var response = EventConsumersDto.FromResults(eventConsumers.Value, Resources);
return Ok(response);
}
@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
{
var eventConsumer = await GetGrain().StartAsync(name);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, this);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, Resources);
return Ok(response);
}
@ -61,7 +61,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
{
var eventConsumer = await GetGrain().StopAsync(name);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, this);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, Resources);
return Ok(response);
}
@ -74,7 +74,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
{
var eventConsumer = await GetGrain().ResetAsync(name);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, this);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, Resources);
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.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
@ -24,31 +23,31 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
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());
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 };
if (!IsResetting)
{
AddPutLink("reset", controller.Url<EventConsumersController>(x => nameof(x.ResetEventConsumer), values));
AddPutLink("reset", resources.Url<EventConsumersController>(x => nameof(x.ResetEventConsumer), values));
}
if (IsStopped)
{
AddPutLink("start", controller.Url<EventConsumersController>(x => nameof(x.StartEventConsumer), values));
AddPutLink("start", resources.Url<EventConsumersController>(x => nameof(x.StartEventConsumer), values));
}
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>
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
{
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;
}

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.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models
@ -91,7 +90,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
/// </summary>
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();
@ -103,48 +102,45 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
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)
{
AddPutLink("disable", controller.Url<RulesController>(x => nameof(x.DisableRule), values));
AddPutLink("disable", resources.Url<RulesController>(x => nameof(x.DisableRule), values));
}
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)
{
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))
{
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteRule), values));
AddGetLink("logs", resources.Url<RulesController>(x => nameof(x.GetEvents), 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;

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.Linq;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models
@ -28,36 +27,36 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
/// </summary>
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
{
Items = items.Select(x => RuleDto.FromRule(x, runningRuleId, controller, app)).ToArray()
Items = items.Select(x => RuleDto.FromRule(x, runningRuleId, resources)).ToArray()
};
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)
{
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(() =>
{
return RulesDto.FromRules(rules, runningRuleId, this, app);
return RulesDto.FromRules(rules, runningRuleId, Resources);
});
return Ok(response);
@ -363,7 +363,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(Context.App.Id);
var result = context.Result<IEnrichedRuleEntity>();
var response = RuleDto.FromRule(result, runningRuleId, this, app);
var response = RuleDto.FromRule(result, runningRuleId, Resources);
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;
}
public void CreateLinks(ApiController controller, string app, string schema, bool allowUpdate)
public void CreateLinks(Resources resources, string schema, bool allowUpdate)
{
allowUpdate = allowUpdate && !IsLocked;
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)
{
AddPutLink("show", controller.Url<SchemaFieldsController>(x => nameof(x.ShowField), values));
AddPutLink("show", resources.Url<SchemaFieldsController>(x => nameof(x.ShowField), values));
}
else
{
AddPutLink("hide", controller.Url<SchemaFieldsController>(x => nameof(x.HideField), values));
AddPutLink("hide", resources.Url<SchemaFieldsController>(x => nameof(x.HideField), values));
}
if (IsDisabled)
{
AddPutLink("enable", controller.Url<SchemaFieldsController>(x => nameof(x.EnableField), values));
AddPutLink("enable", resources.Url<SchemaFieldsController>(x => nameof(x.EnableField), values));
}
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)
{
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)
{
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]
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;
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)
{
AddPutLink("show", controller.Url<SchemaFieldsController>(x => nameof(x.ShowNestedField), values));
AddPutLink("show", resources.Url<SchemaFieldsController>(x => nameof(x.ShowNestedField), values));
}
else
{
AddPutLink("hide", controller.Url<SchemaFieldsController>(x => nameof(x.HideNestedField), values));
AddPutLink("hide", resources.Url<SchemaFieldsController>(x => nameof(x.HideNestedField), values));
}
if (IsDisabled)
{
AddPutLink("enable", controller.Url<SchemaFieldsController>(x => nameof(x.EnableNestedField), values));
AddPutLink("enable", resources.Url<SchemaFieldsController>(x => nameof(x.EnableNestedField), values));
}
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 Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
@ -49,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
[Required]
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();
@ -59,6 +58,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
SimpleMapper.Map(schema.SchemaDef.Properties, result.Properties);
result.FieldsInLists = schema.SchemaDef.FieldsInLists.ToList();
result.FieldsInReferences = schema.SchemaDef.FieldsInReferences.ToList();
if (schema.SchemaDef.PreviewUrls.Count > 0)
@ -73,22 +73,22 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
result.Fields.Add(FieldDto.FromField(field));
}
result.CreateLinks(controller, app);
result.CreateLinks(resources);
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)
{
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.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
@ -79,7 +78,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary>
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();
@ -87,64 +86,64 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
SimpleMapper.Map(schema.SchemaDef, result);
SimpleMapper.Map(schema.SchemaDef.Properties, result.Properties);
result.CreateLinks(controller, app);
result.CreateLinks(controller);
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)
{
AddPutLink("unpublish", controller.Url<SchemasController>(x => nameof(x.UnpublishSchema), values));
AddPutLink("unpublish", resources.Url<SchemasController>(x => nameof(x.UnpublishSchema), values));
}
else
{
AddPutLink("publish", controller.Url<SchemasController>(x => nameof(x.PublishSchema), values));
AddPutLink("publish", resources.Url<SchemasController>(x => nameof(x.PublishSchema), values));
}
}
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", controller.Url<SchemaFieldsController>(x => nameof(x.PutSchemaUIFields), values));
AddPutLink("fields/ui", resources.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/category", controller.Url<SchemasController>(x => nameof(x.PutCategory), values));
AddPutLink("update/sync", controller.Url<SchemasController>(x => nameof(x.PutSchemaSync), values));
AddPutLink("update/urls", controller.Url<SchemasController>(x => nameof(x.PutPreviewUrls), values));
AddPutLink("update", resources.Url<SchemasController>(x => nameof(x.PutSchema), values));
AddPutLink("update/sync", resources.Url<SchemasController>(x => nameof(x.PutSchemaSync), values));
AddPutLink("update/urls", resources.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.Linq;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
@ -20,25 +19,25 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary>
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
{
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;

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 response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
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 response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
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 response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -125,7 +125,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = request.ToCommand();
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -151,7 +151,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = request.ToCommand(parentId);
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -177,7 +177,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = request.ToCommand(id);
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -204,7 +204,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = request.ToCommand(id, parentId);
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -231,7 +231,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new LockField { FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -259,7 +259,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new LockField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -286,7 +286,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new HideField { FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -314,7 +314,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new HideField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -341,7 +341,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new ShowField { FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -369,7 +369,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new ShowField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -396,7 +396,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new EnableField { FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -424,7 +424,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new EnableField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -451,7 +451,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new DisableField { FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -479,7 +479,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new DisableField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -504,7 +504,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new DeleteField { FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -530,17 +530,17 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var command = new DeleteField { ParentFieldId = parentId, FieldId = id };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
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 result = context.Result<ISchemaEntity>();
var response = SchemaDetailsDto.FromSchemaWithDetails(result, this, app);
var response = SchemaDetailsDto.FromSchemaWithDetails(result, Resources);
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(() =>
{
return SchemasDto.FromSchemas(schemas, this, app);
return SchemasDto.FromSchemas(schemas, Resources);
});
Response.Headers[HeaderNames.ETag] = schemas.ToEtag();
@ -94,7 +94,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
var response = Deferred.Response(() =>
{
return SchemaDetailsDto.FromSchemaWithDetails(schema, this, app);
return SchemaDetailsDto.FromSchemaWithDetails(schema, Resources);
});
Response.Headers[HeaderNames.ETag] = schema.ToEtag();
@ -320,7 +320,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
var context = await CommandBus.PublishAsync(command);
var result = context.Result<ISchemaEntity>();
var response = SchemaDetailsDto.FromSchemaWithDetails(result, this, app);
var response = SchemaDetailsDto.FromSchemaWithDetails(result, Resources);
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.Languages;
using Squidex.Areas.Api.Controllers.Ping;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Users.Models
{
public sealed class ResourcesDto : Resource
{
public static ResourcesDto FromController(ApiController controller)
public static ResourcesDto FromResources(Resources resources)
{
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("languages", controller.Url<LanguagesController>(x => nameof(x.GetLanguages)));
result.AddGetLink("languages", resources.Url<LanguagesController>(x => nameof(x.GetLanguages)));
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.Shared.Users;
using Squidex.Web;
using AllPermissions = Squidex.Shared.Permissions;
namespace Squidex.Areas.Api.Controllers.Users.Models
{
@ -46,48 +45,48 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
[Required]
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 userName = user.DisplayName()!;
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 };
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
{
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;
}

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.Linq;
using Squidex.Domain.Users;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Users.Models
@ -27,24 +26,24 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
[Required]
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
{
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;

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

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

@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission]
public IActionResult GetUserResources()
{
var response = ResourcesDto.FromController(this);
var response = ResourcesDto.FromResources(Resources);
return Ok(response);
}
@ -97,7 +97,7 @@ namespace Squidex.Areas.Api.Controllers.Users
{
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);
}
@ -131,7 +131,7 @@ namespace Squidex.Areas.Api.Controllers.Users
if (entity != null)
{
var response = UserDto.FromUser(entity, this);
var response = UserDto.FromUser(entity, Resources);
return Ok(response);
}

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

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

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

@ -38,24 +38,24 @@ namespace Squidex.Config.Authentication
apiAuthorityUrl = urlsOptions.BuildUrl(Constants.IdentityServerPrefix);
}
authBuilder.AddIdentityServerAuthentication(options =>
if (identityOptions.LocalApi)
{
authBuilder.AddLocalApi(Constants.ApiSecurityScheme, options =>
{
options.ExpectedScope = apiScope;
});
}
else
{
authBuilder.AddIdentityServerAuthentication(Constants.ApiSecurityScheme, options =>
{
options.Authority = apiAuthorityUrl;
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);
return result;
};
});
}
authBuilder.AddOpenIdConnect(options =>
{

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

@ -55,6 +55,8 @@ namespace Squidex.Config
public bool AllowPasswordAuth { get; set; }
public bool LocalApi { get; set; } = true;
public bool LockAutomatically { 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>()
.AsSelf();
services.AddSingletonAs<SchemaResolver>()
.AsSelf();
services.AddSingletonAs<RobotsTxtMiddleware>()
.AsSelf();
@ -77,6 +80,7 @@ namespace Squidex.Config.Web
options.Filters.Add<CachingFilter>();
options.Filters.Add<DeferredActionFilter>();
options.Filters.Add<AppResolver>();
options.Filters.Add<SchemaResolver>();
options.Filters.Add<MeasureResultFilter>();
})
.AddRazorRuntimeCompilation()

6
backend/src/Squidex/appsettings.json

@ -504,11 +504,15 @@
/*
* 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.
*/
"allowPasswordAuth": true,
/*
* Uses local API authentication.
*/
"localApi": true,
/*
* Initial admin user.
*/

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
@ -13,8 +14,10 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Web.Pipeline;
using Xunit;
#pragma warning disable IDE0017 // Simplify object initialization
@ -49,17 +52,17 @@ namespace Squidex.Web
}
[Fact]
public void Should_use_bearer_schemes()
public void Should_use_custom_authorization_scheme()
{
var sut = new ApiPermissionAttribute();
Assert.Equal("Bearer", sut.AuthenticationSchemes);
Assert.Equal(Constants.ApiSecurityScheme, sut.AuthenticationSchemes);
}
[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"));
@ -71,10 +74,26 @@ namespace Squidex.Web
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]
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"));

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