Browse Source

Feature/own content (#632)

* Return contents with permission read.own

Co-authored-by: Dmitriy Borowskiy <d.borowskij@gmail.com>
pull/634/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
382ef7f357
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/i18n/source/backend_en.json
  2. 12
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs
  3. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs
  4. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs
  6. 36
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/GuardContent.cs
  7. 14
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Entities/Q.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs
  10. 7
      backend/src/Squidex.Shared/Permissions.cs
  11. 3
      backend/src/Squidex.Shared/Texts.it.resx
  12. 3
      backend/src/Squidex.Shared/Texts.nl.resx
  13. 3
      backend/src/Squidex.Shared/Texts.resx
  14. 10
      backend/src/Squidex.Web/Resources.cs
  15. 20
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  16. 12
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs
  17. 1
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  18. 28
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs
  19. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs
  20. 117
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs
  21. 30
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs
  22. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs
  23. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs

1
backend/i18n/source/backend_en.json

@ -51,6 +51,7 @@
"common.displayName": "Display name", "common.displayName": "Display name",
"common.editor": "Editor", "common.editor": "Editor",
"common.email": "Email", "common.email": "Email",
"common.errorNoPermission": "You do not have the necessary permission.",
"common.field": "Field", "common.field": "Field",
"common.fieldIds": "Field IDs", "common.fieldIds": "Field IDs",
"common.fieldName": "Field name", "common.fieldName": "Field name",

12
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs

@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
var query = q.Query.AdjustToModel(app.Id); var query = q.Query.AdjustToModel(app.Id);
var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), query, q.Reference); var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), query, q.Reference, q.CreatedBy);
var contentEntities = await FindContentsAsync(query, filter); var contentEntities = await FindContentsAsync(query, filter);
var contentTotal = (long)contentEntities.Count; var contentTotal = (long)contentEntities.Count;
@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
var query = q.Query.AdjustToModel(app.Id); var query = q.Query.AdjustToModel(app.Id);
var filter = CreateFilter(schema.AppId.Id, Enumerable.Repeat(schema.Id, 1), query, q.Reference); var filter = CreateFilter(schema.AppId.Id, Enumerable.Repeat(schema.Id, 1), query, q.Reference, q.CreatedBy);
var contentEntities = await FindContentsAsync(query, filter); var contentEntities = await FindContentsAsync(query, filter);
var contentTotal = (long)contentEntities.Count; var contentTotal = (long)contentEntities.Count;
@ -216,7 +216,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
return Filter.And(filters); return Filter.And(filters);
} }
private static FilterDefinition<MongoContentEntity> CreateFilter(DomainId appId, IEnumerable<DomainId> schemaIds, ClrQuery? query, DomainId referenced) private static FilterDefinition<MongoContentEntity> CreateFilter(DomainId appId, IEnumerable<DomainId> schemaIds, ClrQuery? query,
DomainId referenced, RefToken? createdBy)
{ {
var filters = new List<FilterDefinition<MongoContentEntity>> var filters = new List<FilterDefinition<MongoContentEntity>>
{ {
@ -235,6 +236,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced)); filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced));
} }
if (createdBy != null)
{
filters.Add(Filter.Eq(x => x.CreatedBy, createdBy));
}
return Filter.And(filters); return Filter.And(filters);
} }
} }

10
backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs

@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var command = new UpdateContent { Data = job.Data! }; var command = new UpdateContent { Data = job.Data! };
await EnrichAsync(id, task, command, Permissions.AppContentsUpdate); await EnrichAsync(id, task, command, Permissions.AppContentsUpdateOwn);
return command; return command;
} }
@ -216,7 +216,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var command = new PatchContent { Data = job.Data! }; var command = new PatchContent { Data = job.Data! };
await EnrichAsync(id, task, command, Permissions.AppContentsUpdate); await EnrichAsync(id, task, command, Permissions.AppContentsUpdateOwn);
return command; return command;
} }
@ -224,7 +224,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var command = new ValidateContent(); var command = new ValidateContent();
await EnrichAsync(id, task, command, Permissions.AppContentsRead); await EnrichAsync(id, task, command, Permissions.AppContentsReadOwn);
return command; return command;
} }
@ -232,7 +232,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var command = new ChangeContentStatus { Status = job.Status, DueTime = job.DueTime }; var command = new ChangeContentStatus { Status = job.Status, DueTime = job.DueTime };
await EnrichAsync(id, task, command, Permissions.AppContentsUpdate); await EnrichAsync(id, task, command, Permissions.AppContentsUpdateOwn);
return command; return command;
} }
@ -240,7 +240,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var command = new DeleteContent(); var command = new DeleteContent();
await EnrichAsync(id, task, command, Permissions.AppContentsDelete); await EnrichAsync(id, task, command, Permissions.AppContentsDeleteOwn);
return command; return command;
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs

@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private static bool HasPermission(Context context, string schemaName) private static bool HasPermission(Context context, string schemaName)
{ {
var permission = Permissions.ForApp(Permissions.AppContentsRead, context.App.Name, schemaName); var permission = Permissions.ForApp(Permissions.AppContentsReadOwn, context.App.Name, schemaName);
return context.Permissions.Allows(permission); return context.Permissions.Allows(permission);
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs

@ -129,6 +129,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{ {
await LoadContext(c); await LoadContext(c);
GuardContent.CanValidate(c, Snapshot);
await context.ValidateContentAndInputAsync(Snapshot.Data); await context.ValidateContentAndInputAsync(Snapshot.Data);
return true; return true;

36
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/GuardContent.cs

@ -15,6 +15,8 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Squidex.Shared;
using Squidex.Shared.Identity;
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
@ -46,6 +48,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
CheckPermission(content, command, Permissions.AppContentsUpdate);
Validate.It(e => Validate.It(e =>
{ {
ValidateData(command, e); ValidateData(command, e);
@ -60,6 +64,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
CheckPermission(content, command, Permissions.AppContentsUpdate);
Validate.It(e => Validate.It(e =>
{ {
ValidateData(command, e); ValidateData(command, e);
@ -68,10 +74,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
await ValidateCanUpdate(content, contentWorkflow, command.User); await ValidateCanUpdate(content, contentWorkflow, command.User);
} }
public static void CanValidate(ValidateContent command, IContentEntity content)
{
Guard.NotNull(command, nameof(command));
CheckPermission(content, command, Permissions.AppContentsRead);
}
public static void CanDeleteDraft(DeleteContentDraft command, IContentEntity content) public static void CanDeleteDraft(DeleteContentDraft command, IContentEntity content)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
CheckPermission(content, command, Permissions.AppContentsVersionDelete);
if (content.NewStatus == null) if (content.NewStatus == null)
{ {
throw new DomainException(T.Get("contents.draftToDeleteNotFound")); throw new DomainException(T.Get("contents.draftToDeleteNotFound"));
@ -82,6 +97,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
CheckPermission(content, command, Permissions.AppContentsVersionCreate);
if (content.Status != Status.Published) if (content.Status != Status.Published)
{ {
throw new DomainException(T.Get("contents.draftNotCreateForUnpublished")); throw new DomainException(T.Get("contents.draftNotCreateForUnpublished"));
@ -96,6 +113,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
CheckPermission(content, command, Permissions.AppContentsChangeStatus);
if (schema.SchemaDef.IsSingleton) if (schema.SchemaDef.IsSingleton)
{ {
if (content.NewStatus == null || command.Status != Status.Published) if (content.NewStatus == null || command.Status != Status.Published)
@ -141,6 +160,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
CheckPermission(content, command, Permissions.AppContentsDeleteOwn);
if (schema.SchemaDef.IsSingleton) if (schema.SchemaDef.IsSingleton)
{ {
throw new DomainException(T.Get("contents.singletonNotDeletable")); throw new DomainException(T.Get("contents.singletonNotDeletable"));
@ -174,5 +195,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
throw new DomainException(T.Get("contents.workflowErrorUpdate", new { status })); throw new DomainException(T.Get("contents.workflowErrorUpdate", new { status }));
} }
} }
public static void CheckPermission(IContentEntity content, ContentCommand command, string permission)
{
if (content.CreatedBy?.Equals(command.Actor) == true)
{
return;
}
var requiredPermission = Permissions.ForApp(permission, content.AppId.Name, content.SchemaId.Name);
if (!command.User.Claims.Permissions().Allows(requiredPermission))
{
throw new DomainForbiddenException(T.Get("common.errorNoPermission"));
}
}
} }
} }

14
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using Squidex.Log; using Squidex.Log;
using Squidex.Shared; using Squidex.Shared;
@ -92,6 +93,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
if (!HasPermission(context, schema, Permissions.AppContentsRead))
{
q.CreatedBy = context.User.Token();
}
using (Profiler.TraceMethod<ContentQueryService>()) using (Profiler.TraceMethod<ContentQueryService>())
{ {
q = await queryParser.ParseAsync(context, q, schema); q = await queryParser.ParseAsync(context, q, schema);
@ -193,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName, canCache); schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName, canCache);
} }
if (schema != null && !HasPermission(context, schema)) if (schema != null && !HasPermission(context, schema, Permissions.AppContentsReadOwn))
{ {
throw new DomainForbiddenException(T.Get("schemas.noPermission")); throw new DomainForbiddenException(T.Get("schemas.noPermission"));
} }
@ -205,12 +211,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var schemas = await appProvider.GetSchemasAsync(context.App.Id); var schemas = await appProvider.GetSchemasAsync(context.App.Id);
return schemas.Where(x => HasPermission(context, x)).ToList(); return schemas.Where(x => HasPermission(context, x, Permissions.AppContentsReadOwn)).ToList();
} }
private static bool HasPermission(Context context, ISchemaEntity schema) private static bool HasPermission(Context context, ISchemaEntity schema, string permissionId)
{ {
var permission = Permissions.ForApp(Permissions.AppContentsRead, context.App.Name, schema.SchemaDef.Name); var permission = Permissions.ForApp(permissionId, context.App.Name, schema.SchemaDef.Name);
return context.Permissions.Allows(permission); return context.Permissions.Allows(permission);
} }

2
backend/src/Squidex.Domain.Apps.Entities/Q.cs

@ -31,6 +31,8 @@ namespace Squidex.Domain.Apps.Entities
public ClrQuery Query { get; init; } = new ClrQuery(); public ClrQuery Query { get; init; } = new ClrQuery();
public RefToken? CreatedBy { get; set; }
private Q() private Q()
{ {
} }

2
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs

@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
private static bool HasPermission(Context context, NamedId<DomainId> schemaId) private static bool HasPermission(Context context, NamedId<DomainId> schemaId)
{ {
var permission = Permissions.ForApp(Permissions.AppContentsRead, context.App.Name, schemaId.Name); var permission = Permissions.ForApp(Permissions.AppContentsReadOwn, context.App.Name, schemaId.Name);
return context.Permissions.Allows(permission); return context.Permissions.Allows(permission);
} }

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

@ -137,12 +137,19 @@ namespace Squidex.Shared
public const string AppContents = "squidex.apps.{app}.contents.{name}"; public const string AppContents = "squidex.apps.{app}.contents.{name}";
public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read"; public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read";
public const string AppContentsReadOwn = "squidex.apps.{app}.contents.{name}.read.own";
public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create"; public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create";
public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update"; public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update";
public const string AppContentsUpdateOwn = "squidex.apps.{app}.contents.{name}.update.own";
public const string AppContentsChangeStatus = "squidex.apps.{app}.contents.{name}.changestatus";
public const string AppContentsChangeStatusOwn = "squidex.apps.{app}.contents.{name}.changestatus.own";
public const string AppContentsUpsert = "squidex.apps.{app}.contents.{name}.upsert"; public const string AppContentsUpsert = "squidex.apps.{app}.contents.{name}.upsert";
public const string AppContentsVersionCreate = "squidex.apps.{app}.contents.{name}.version.create"; public const string AppContentsVersionCreate = "squidex.apps.{app}.contents.{name}.version.create";
public const string AppContentsVersionCreateOwn = "squidex.apps.{app}.contents.{name}.version.create.own";
public const string AppContentsVersionDelete = "squidex.apps.{app}.contents.{name}.version.delete"; public const string AppContentsVersionDelete = "squidex.apps.{app}.contents.{name}.version.delete";
public const string AppContentsVersionDeleteOwn = "squidex.apps.{app}.contents.{name}.version.delete.own";
public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete"; public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete";
public const string AppContentsDeleteOwn = "squidex.apps.{app}.contents.{name}.delete.own";
static Permissions() static Permissions()
{ {

3
backend/src/Squidex.Shared/Texts.it.resx

@ -238,6 +238,9 @@
<data name="common.email" xml:space="preserve"> <data name="common.email" xml:space="preserve">
<value>Email</value> <value>Email</value>
</data> </data>
<data name="common.errorNoPermission" xml:space="preserve">
<value>You do not have the necessary permission.</value>
</data>
<data name="common.field" xml:space="preserve"> <data name="common.field" xml:space="preserve">
<value>Campo</value> <value>Campo</value>
</data> </data>

3
backend/src/Squidex.Shared/Texts.nl.resx

@ -238,6 +238,9 @@
<data name="common.email" xml:space="preserve"> <data name="common.email" xml:space="preserve">
<value>E-mail</value> <value>E-mail</value>
</data> </data>
<data name="common.errorNoPermission" xml:space="preserve">
<value>You do not have the necessary permission.</value>
</data>
<data name="common.field" xml:space="preserve"> <data name="common.field" xml:space="preserve">
<value>Veld</value> <value>Veld</value>
</data> </data>

3
backend/src/Squidex.Shared/Texts.resx

@ -238,6 +238,9 @@
<data name="common.email" xml:space="preserve"> <data name="common.email" xml:space="preserve">
<value>Email</value> <value>Email</value>
</data> </data>
<data name="common.errorNoPermission" xml:space="preserve">
<value>You do not have the necessary permission.</value>
</data>
<data name="common.field" xml:space="preserve"> <data name="common.field" xml:space="preserve">
<value>Field</value> <value>Field</value>
</data> </data>

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

@ -20,17 +20,17 @@ namespace Squidex.Web
private readonly Dictionary<(string, string), bool> schemaPermissions = new Dictionary<(string, string), bool>(); private readonly Dictionary<(string, string), bool> schemaPermissions = new Dictionary<(string, string), bool>();
// Contents // Contents
public bool CanReadContent(string schema) => IsAllowedForSchema(P.AppContentsRead, schema); public bool CanReadContent(string schema) => IsAllowedForSchema(P.AppContentsReadOwn, schema);
public bool CanCreateContent(string schema) => IsAllowedForSchema(P.AppContentsCreate, schema); public bool CanCreateContent(string schema) => IsAllowedForSchema(P.AppContentsCreate, schema);
public bool CanCreateContentVersion(string schema) => IsAllowedForSchema(P.AppContentsVersionCreate, schema); public bool CanCreateContentVersion(string schema) => IsAllowedForSchema(P.AppContentsVersionCreateOwn, schema);
public bool CanDeleteContent(string schema) => IsAllowedForSchema(P.AppContentsDelete, schema); public bool CanDeleteContent(string schema) => IsAllowedForSchema(P.AppContentsDeleteOwn, schema);
public bool CanDeleteContentVersion(string schema) => IsAllowedForSchema(P.AppContentsVersionDelete, schema); public bool CanDeleteContentVersion(string schema) => IsAllowedForSchema(P.AppContentsVersionDeleteOwn, schema);
public bool CanUpdateContent(string schema) => IsAllowedForSchema(P.AppContentsUpdate, schema); public bool CanUpdateContent(string schema) => IsAllowedForSchema(P.AppContentsUpdateOwn, schema);
// Schemas // Schemas
public bool CanUpdateSchema(string schema) => IsAllowedForSchema(P.AppSchemasDelete, schema); public bool CanUpdateSchema(string schema) => IsAllowedForSchema(P.AppSchemasDelete, schema);

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

@ -54,7 +54,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("content/{app}/graphql/")] [Route("content/{app}/graphql/")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous(Permissions.AppContents)]
[ApiCosts(2)] [ApiCosts(2)]
public async Task<IActionResult> GetGraphQL(string app, [FromQuery] GraphQLGetDto? queries = null) public async Task<IActionResult> GetGraphQL(string app, [FromQuery] GraphQLGetDto? queries = null)
{ {
@ -86,7 +86,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("content/{app}/graphql/")] [Route("content/{app}/graphql/")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous(Permissions.AppContents)]
[ApiCosts(2)] [ApiCosts(2)]
public async Task<IActionResult> PostGraphQL(string app, [FromBody] GraphQLPostDto query) public async Task<IActionResult> PostGraphQL(string app, [FromBody] GraphQLPostDto query)
{ {
@ -118,7 +118,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("content/{app}/graphql/batch")] [Route("content/{app}/graphql/batch")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous(Permissions.AppContents)]
[ApiCosts(2)] [ApiCosts(2)]
public async Task<IActionResult> PostGraphQLBatch(string app, [FromBody] GraphQLPostDto[] batch) public async Task<IActionResult> PostGraphQLBatch(string app, [FromBody] GraphQLPostDto[] batch)
{ {
@ -396,7 +396,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("content/{app}/{name}/{id}/{version}/")] [Route("content/{app}/{name}/{id}/{version}/")]
[ApiPermissionOrAnonymous(Permissions.AppContentsRead)] [ApiPermissionOrAnonymous(Permissions.AppContentsReadOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContentVersion(string app, string name, DomainId id, int version) public async Task<IActionResult> GetContentVersion(string app, string name, DomainId id, int version)
{ {
@ -557,7 +557,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpPut] [HttpPut]
[Route("content/{app}/{name}/{id}/")] [Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpdateOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutContent(string app, string name, DomainId id, [FromBody] ContentData request) public async Task<IActionResult> PutContent(string app, string name, DomainId id, [FromBody] ContentData request)
{ {
@ -586,7 +586,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpPatch] [HttpPatch]
[Route("content/{app}/{name}/{id}/")] [Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpdateOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PatchContent(string app, string name, DomainId id, [FromBody] ContentData request) public async Task<IActionResult> PatchContent(string app, string name, DomainId id, [FromBody] ContentData request)
{ {
@ -615,7 +615,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpPut] [HttpPut]
[Route("content/{app}/{name}/{id}/status/")] [Route("content/{app}/{name}/{id}/status/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiPermissionOrAnonymous(Permissions.AppContentsChangeStatusOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutContentStatus(string app, string name, DomainId id, ChangeStatusDto request) public async Task<IActionResult> PutContentStatus(string app, string name, DomainId id, ChangeStatusDto request)
{ {
@ -642,7 +642,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpPost] [HttpPost]
[Route("content/{app}/{name}/{id}/draft/")] [Route("content/{app}/{name}/{id}/draft/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsVersionCreate)] [ApiPermissionOrAnonymous(Permissions.AppContentsVersionCreateOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> CreateDraft(string app, string name, DomainId id) public async Task<IActionResult> CreateDraft(string app, string name, DomainId id)
{ {
@ -669,7 +669,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpDelete] [HttpDelete]
[Route("content/{app}/{name}/{id}/draft/")] [Route("content/{app}/{name}/{id}/draft/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppContentsDelete)] [ApiPermissionOrAnonymous(Permissions.AppContentsDeleteOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteVersion(string app, string name, DomainId id) public async Task<IActionResult> DeleteVersion(string app, string name, DomainId id)
{ {
@ -697,7 +697,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks> /// </remarks>
[HttpDelete] [HttpDelete]
[Route("content/{app}/{name}/{id}/")] [Route("content/{app}/{name}/{id}/")]
[ApiPermissionOrAnonymous(Permissions.AppContentsDelete)] [ApiPermissionOrAnonymous(Permissions.AppContentsDeleteOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, DomainId id, [FromQuery] bool checkReferrers = false) public async Task<IActionResult> DeleteContent(string app, string name, DomainId id, [FromQuery] bool checkReferrers = false)
{ {

12
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs

@ -85,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private OpenApiPathItem GenerateSchemaGetsOperation() private OpenApiPathItem GenerateSchemaGetsOperation()
{ {
return Add(OpenApiOperationMethod.Get, Permissions.AppContentsRead, "/", return Add(OpenApiOperationMethod.Get, Permissions.AppContentsReadOwn, "/",
operation => operation =>
{ {
operation.OperationId = $"Query{schemaType}Contents"; operation.OperationId = $"Query{schemaType}Contents";
@ -103,7 +103,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private OpenApiPathItem GenerateSchemaGetOperation() private OpenApiPathItem GenerateSchemaGetOperation()
{ {
return Add(OpenApiOperationMethod.Get, Permissions.AppContentsRead, "/{id}", operation => return Add(OpenApiOperationMethod.Get, Permissions.AppContentsReadOwn, "/{id}", operation =>
{ {
operation.OperationId = $"Get{schemaType}Content"; operation.OperationId = $"Get{schemaType}Content";
@ -133,7 +133,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private OpenApiPathItem GenerateSchemaUpdateOperation() private OpenApiPathItem GenerateSchemaUpdateOperation()
{ {
return Add(OpenApiOperationMethod.Put, Permissions.AppContentsUpdate, "/{id}", return Add(OpenApiOperationMethod.Put, Permissions.AppContentsUpdateOwn, "/{id}",
operation => operation =>
{ {
operation.OperationId = $"Update{schemaType}Content"; operation.OperationId = $"Update{schemaType}Content";
@ -149,7 +149,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private OpenApiPathItem GenerateSchemaUpdatePatchOperation() private OpenApiPathItem GenerateSchemaUpdatePatchOperation()
{ {
return Add(OpenApiOperationMethod.Patch, Permissions.AppContentsUpdate, "/{id}", return Add(OpenApiOperationMethod.Patch, Permissions.AppContentsUpdateOwn, "/{id}",
operation => operation =>
{ {
operation.OperationId = $"Path{schemaType}Content"; operation.OperationId = $"Path{schemaType}Content";
@ -165,7 +165,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private OpenApiPathItem GenerateSchemaStatusOperation() private OpenApiPathItem GenerateSchemaStatusOperation()
{ {
return Add(OpenApiOperationMethod.Put, Permissions.AppContentsUpdate, "/{id}/status", return Add(OpenApiOperationMethod.Put, Permissions.AppContentsUpdateOwn, "/{id}/status",
operation => operation =>
{ {
operation.OperationId = $"Change{schemaType}ContentStatus"; operation.OperationId = $"Change{schemaType}ContentStatus";
@ -181,7 +181,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private OpenApiPathItem GenerateSchemaDeleteOperation() private OpenApiPathItem GenerateSchemaDeleteOperation()
{ {
return Add(OpenApiOperationMethod.Delete, Permissions.AppContentsDelete, "/{id}", return Add(OpenApiOperationMethod.Delete, Permissions.AppContentsDeleteOwn, "/{id}",
operation => operation =>
{ {
operation.OperationId = $"Delete{schemaType}Content"; operation.OperationId = $"Delete{schemaType}Content";

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

@ -180,7 +180,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
if (content.CanUpdate && resources.CanUpdateContent(schema)) if (content.CanUpdate && resources.CanUpdateContent(schema))
{ {
AddPutLink("update", resources.Url<ContentsController>(x => nameof(x.PutContent), values)); AddPutLink("update", resources.Url<ContentsController>(x => nameof(x.PutContent), values));
AddPatchLink("patch", resources.Url<ContentsController>(x => nameof(x.PatchContent), values)); AddPatchLink("patch", resources.Url<ContentsController>(x => nameof(x.PatchContent), values));
} }

28
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs

@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_throw_exception_when_content_cannot_be_resolved() public async Task Should_throw_exception_when_content_cannot_be_resolved()
{ {
SetupContext(Permissions.AppContentsUpdate); SetupContext(Permissions.AppContentsUpdateOwn);
var (_, _, query) = CreateTestData(true); var (_, _, query) = CreateTestData(true);
@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_throw_exception_when_query_resolves_multiple_contents() public async Task Should_throw_exception_when_query_resolves_multiple_contents()
{ {
var requestContext = SetupContext(Permissions.AppContentsUpdate); var requestContext = SetupContext(Permissions.AppContentsUpdateOwn);
var (id, data, query) = CreateTestData(true); var (id, data, query) = CreateTestData(true);
@ -240,7 +240,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_throw_security_exception_when_user_has_no_permission_for_creating() public async Task Should_throw_security_exception_when_user_has_no_permission_for_creating()
{ {
SetupContext(Permissions.AppContentsRead); SetupContext(Permissions.AppContentsReadOwn);
var (id, data, _) = CreateTestData(false); var (id, data, _) = CreateTestData(false);
@ -257,7 +257,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_update_content() public async Task Should_update_content()
{ {
SetupContext(Permissions.AppContentsUpdate); SetupContext(Permissions.AppContentsUpdateOwn);
var (id, data, _) = CreateTestData(false); var (id, data, _) = CreateTestData(false);
@ -275,7 +275,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_throw_security_exception_when_user_has_no_permission_for_updating() public async Task Should_throw_security_exception_when_user_has_no_permission_for_updating()
{ {
SetupContext(Permissions.AppContentsRead); SetupContext(Permissions.AppContentsReadOwn);
var (id, data, _) = CreateTestData(false); var (id, data, _) = CreateTestData(false);
@ -292,7 +292,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_patch_content() public async Task Should_patch_content()
{ {
SetupContext(Permissions.AppContentsUpdate); SetupContext(Permissions.AppContentsUpdateOwn);
var (id, data, _) = CreateTestData(false); var (id, data, _) = CreateTestData(false);
@ -310,7 +310,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_throw_security_exception_when_user_has_no_permission_for_patching() public async Task Should_throw_security_exception_when_user_has_no_permission_for_patching()
{ {
SetupContext(Permissions.AppContentsRead); SetupContext(Permissions.AppContentsReadOwn);
var (id, data, _) = CreateTestData(false); var (id, data, _) = CreateTestData(false);
@ -327,7 +327,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_change_content_status() public async Task Should_change_content_status()
{ {
SetupContext(Permissions.AppContentsUpdate); SetupContext(Permissions.AppContentsUpdateOwn);
var (id, _, _) = CreateTestData(false); var (id, _, _) = CreateTestData(false);
@ -344,7 +344,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_change_content_status_with_due_time() public async Task Should_change_content_status_with_due_time()
{ {
SetupContext(Permissions.AppContentsUpdate); SetupContext(Permissions.AppContentsUpdateOwn);
var time = Instant.FromDateTimeUtc(DateTime.UtcNow); var time = Instant.FromDateTimeUtc(DateTime.UtcNow);
@ -363,7 +363,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_throw_security_exception_when_user_has_no_permission_for_changing_status() public async Task Should_throw_security_exception_when_user_has_no_permission_for_changing_status()
{ {
SetupContext(Permissions.AppContentsRead); SetupContext(Permissions.AppContentsReadOwn);
var (id, _, _) = CreateTestData(false); var (id, _, _) = CreateTestData(false);
@ -380,7 +380,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_validate_content() public async Task Should_validate_content()
{ {
SetupContext(Permissions.AppContentsRead); SetupContext(Permissions.AppContentsReadOwn);
var (id, _, _) = CreateTestData(false); var (id, _, _) = CreateTestData(false);
@ -398,7 +398,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_throw_security_exception_when_user_has_no_permission_for_validation() public async Task Should_throw_security_exception_when_user_has_no_permission_for_validation()
{ {
SetupContext(Permissions.AppContentsDelete); SetupContext(Permissions.AppContentsDeleteOwn);
var (id, _, _) = CreateTestData(false); var (id, _, _) = CreateTestData(false);
@ -415,7 +415,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_delete_content() public async Task Should_delete_content()
{ {
SetupContext(Permissions.AppContentsDelete); SetupContext(Permissions.AppContentsDeleteOwn);
var (id, _, _) = CreateTestData(false); var (id, _, _) = CreateTestData(false);
@ -433,7 +433,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_throw_security_exception_when_user_has_no_permission_for_deletion() public async Task Should_throw_security_exception_when_user_has_no_permission_for_deletion()
{ {
SetupContext(Permissions.AppContentsRead); SetupContext(Permissions.AppContentsReadOwn);
var (id, _, _) = CreateTestData(false); var (id, _, _) = CreateTestData(false);

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs

@ -211,7 +211,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
foreach (var schemaId in allowedSchemas) foreach (var schemaId in allowedSchemas)
{ {
var permission = Permissions.ForApp(Permissions.AppContentsRead, appId.Name, schemaId.Name).Id; var permission = Permissions.ForApp(Permissions.AppContentsReadOwn, appId.Name, schemaId.Name).Id;
claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission));
} }

117
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs

@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Squidex.Shared;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
@ -27,8 +28,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(); private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema");
private readonly ClaimsPrincipal user = Mocks.FrontendUser(); private readonly ClaimsPrincipal user = Mocks.FrontendUser();
private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1)); private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1));
private readonly RefToken actor = new RefToken(RefTokenType.Subject, "123");
[Fact] [Fact]
public async Task CanCreate_should_throw_exception_if_data_is_null() public async Task CanCreate_should_throw_exception_if_data_is_null()
@ -101,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
SetupCanUpdate(true); SetupCanUpdate(true);
var content = CreateContent(Status.Draft); var content = CreateContent(Status.Draft);
var command = new UpdateContent(); var command = CreateCommand(new UpdateContent());
await ValidationAssert.ThrowsAsync(() => GuardContent.CanUpdate(command, content, contentWorkflow), await ValidationAssert.ThrowsAsync(() => GuardContent.CanUpdate(command, content, contentWorkflow),
new ValidationError("Data is required.", "Data")); new ValidationError("Data is required.", "Data"));
@ -113,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
SetupCanUpdate(false); SetupCanUpdate(false);
var content = CreateContent(Status.Draft); var content = CreateContent(Status.Draft);
var command = new UpdateContent { Data = new ContentData() }; var command = CreateCommand(new UpdateContent { Data = new ContentData() });
await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanUpdate(command, content, contentWorkflow)); await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanUpdate(command, content, contentWorkflow));
} }
@ -124,7 +127,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
SetupCanUpdate(true); SetupCanUpdate(true);
var content = CreateContent(Status.Draft); var content = CreateContent(Status.Draft);
var command = new UpdateContent { Data = new ContentData(), User = user }; var command = CreateCommand(new UpdateContent { Data = new ContentData() });
await GuardContent.CanUpdate(command, content, contentWorkflow); await GuardContent.CanUpdate(command, content, contentWorkflow);
} }
@ -135,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
SetupCanUpdate(true); SetupCanUpdate(true);
var content = CreateContent(Status.Draft); var content = CreateContent(Status.Draft);
var command = new PatchContent(); var command = CreateCommand(new PatchContent());
await ValidationAssert.ThrowsAsync(() => GuardContent.CanPatch(command, content, contentWorkflow), await ValidationAssert.ThrowsAsync(() => GuardContent.CanPatch(command, content, contentWorkflow),
new ValidationError("Data is required.", "Data")); new ValidationError("Data is required.", "Data"));
@ -147,7 +150,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
SetupCanUpdate(false); SetupCanUpdate(false);
var content = CreateContent(Status.Draft); var content = CreateContent(Status.Draft);
var command = new PatchContent { Data = new ContentData() }; var command = CreateCommand(new PatchContent { Data = new ContentData() });
await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanPatch(command, content, contentWorkflow)); await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanPatch(command, content, contentWorkflow));
} }
@ -158,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
SetupCanUpdate(true); SetupCanUpdate(true);
var content = CreateContent(Status.Draft); var content = CreateContent(Status.Draft);
var command = new PatchContent { Data = new ContentData(), User = user }; var command = CreateCommand(new PatchContent { Data = new ContentData() });
await GuardContent.CanPatch(command, content, contentWorkflow); await GuardContent.CanPatch(command, content, contentWorkflow);
} }
@ -169,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
var schema = CreateSchema(true); var schema = CreateSchema(true);
var content = CreateContent(Status.Published); var content = CreateContent(Status.Published);
var command = new ChangeContentStatus { Status = Status.Draft }; var command = CreateCommand(new ChangeContentStatus { Status = Status.Draft });
await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanChangeStatus(command, content, contentWorkflow, contentRepository, schema)); await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanChangeStatus(command, content, contentWorkflow, contentRepository, schema));
} }
@ -180,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
var schema = CreateSchema(false); var schema = CreateSchema(false);
var content = CreateContent(Status.Draft); var content = CreateContent(Status.Draft);
var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast, User = user }; var command = CreateCommand(new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast });
A.CallTo(() => contentWorkflow.CanMoveToAsync(content, content.Status, command.Status, user)) A.CallTo(() => contentWorkflow.CanMoveToAsync(content, content.Status, command.Status, user))
.Returns(true); .Returns(true);
@ -195,7 +198,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
var schema = CreateSchema(false); var schema = CreateSchema(false);
var content = CreateContent(Status.Draft); var content = CreateContent(Status.Draft);
var command = new ChangeContentStatus { Status = Status.Published, User = user }; var command = CreateCommand(new ChangeContentStatus { Status = Status.Published });
A.CallTo(() => contentWorkflow.CanMoveToAsync(content, content.Status, command.Status, user)) A.CallTo(() => contentWorkflow.CanMoveToAsync(content, content.Status, command.Status, user))
.Returns(false); .Returns(false);
@ -210,7 +213,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
var schema = CreateSchema(true); var schema = CreateSchema(true);
var content = CreateContent(Status.Published); var content = CreateContent(Status.Published);
var command = new ChangeContentStatus { Status = Status.Draft, User = user }; var command = CreateCommand(new ChangeContentStatus { Status = Status.Draft });
A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, content.Id, SearchScope.Published)) A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, content.Id, SearchScope.Published))
.Returns(true); .Returns(true);
@ -224,7 +227,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
var schema = CreateSchema(true); var schema = CreateSchema(true);
var content = CreateDraftContent(Status.Draft); var content = CreateDraftContent(Status.Draft);
var command = new ChangeContentStatus { Status = Status.Published }; var command = CreateCommand(new ChangeContentStatus { Status = Status.Published });
await GuardContent.CanChangeStatus(command, content, contentWorkflow, contentRepository, schema); await GuardContent.CanChangeStatus(command, content, contentWorkflow, contentRepository, schema);
} }
@ -235,7 +238,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
var schema = CreateSchema(false); var schema = CreateSchema(false);
var content = CreateContent(Status.Draft); var content = CreateContent(Status.Draft);
var command = new ChangeContentStatus { Status = Status.Published, User = user }; var command = CreateCommand(new ChangeContentStatus { Status = Status.Published });
A.CallTo(() => contentWorkflow.CanMoveToAsync(content, content.Status, command.Status, user)) A.CallTo(() => contentWorkflow.CanMoveToAsync(content, content.Status, command.Status, user))
.Returns(true); .Returns(true);
@ -246,10 +249,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
[Fact] [Fact]
public void CreateDraft_should_throw_exception_if_not_published() public void CreateDraft_should_throw_exception_if_not_published()
{ {
CreateSchema(false);
var content = CreateContent(Status.Draft); var content = CreateContent(Status.Draft);
var command = new CreateContentDraft(); var command = CreateCommand(new CreateContentDraft());
Assert.Throws<DomainException>(() => GuardContent.CanCreateDraft(command, content)); Assert.Throws<DomainException>(() => GuardContent.CanCreateDraft(command, content));
} }
@ -258,7 +259,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
public void CreateDraft_should_not_throw_exception() public void CreateDraft_should_not_throw_exception()
{ {
var content = CreateContent(Status.Published); var content = CreateContent(Status.Published);
var command = new CreateContentDraft(); var command = CreateCommand(new CreateContentDraft());
GuardContent.CanCreateDraft(command, content); GuardContent.CanCreateDraft(command, content);
} }
@ -266,10 +267,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
[Fact] [Fact]
public void CanDeleteDraft_should_throw_exception_if_no_draft_found() public void CanDeleteDraft_should_throw_exception_if_no_draft_found()
{ {
CreateSchema(false); var schema = CreateSchema(false);
var content = CreateContent(Status.Published); var content = CreateContent(Status.Published);
var command = new DeleteContentDraft(); var command = CreateCommand(new DeleteContentDraft());
Assert.Throws<DomainException>(() => GuardContent.CanDeleteDraft(command, content)); Assert.Throws<DomainException>(() => GuardContent.CanDeleteDraft(command, content));
} }
@ -278,7 +279,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
public void CanDeleteDraft_should_not_throw_exception() public void CanDeleteDraft_should_not_throw_exception()
{ {
var content = CreateDraftContent(Status.Draft); var content = CreateDraftContent(Status.Draft);
var command = new DeleteContentDraft(); var command = CreateCommand(new DeleteContentDraft());
GuardContent.CanDeleteDraft(command, content); GuardContent.CanDeleteDraft(command, content);
} }
@ -289,7 +290,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
var schema = CreateSchema(true); var schema = CreateSchema(true);
var content = CreateContent(Status.Published); var content = CreateContent(Status.Published);
var command = new DeleteContent(); var command = CreateCommand(new DeleteContent());
await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanDelete(command, content, contentRepository, schema)); await Assert.ThrowsAsync<DomainException>(() => GuardContent.CanDelete(command, content, contentRepository, schema));
} }
@ -300,7 +301,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
var schema = CreateSchema(true); var schema = CreateSchema(true);
var content = CreateContent(Status.Published); var content = CreateContent(Status.Published);
var command = new DeleteContent(); var command = CreateCommand(new DeleteContent());
A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, content.Id, SearchScope.All)) A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, content.Id, SearchScope.All))
.Returns(true); .Returns(true);
@ -314,11 +315,45 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
var schema = CreateSchema(false); var schema = CreateSchema(false);
var content = CreateContent(Status.Published); var content = CreateContent(Status.Published);
var command = new DeleteContent(); var command = CreateCommand(new DeleteContent());
await GuardContent.CanDelete(command, content, contentRepository, schema); await GuardContent.CanDelete(command, content, contentRepository, schema);
} }
[Fact]
public void CheckPermission_should_not_throw_exception_if_content_is_from_current_user()
{
var content = CreateContent(status: Status.Published);
var command = CreateCommand(new DeleteContent());
GuardContent.CheckPermission(content, command, Permissions.AppContentsDelete);
}
[Fact]
public void CheckPermission_should_not_throw_exception_if_content_is_from_another_user_but_user_has_permission()
{
var permission = Permissions.ForApp(Permissions.AppContentsDelete, appId.Name, schemaId.Name).Id;
var otherUser = Mocks.FrontendUser(permission: permission);
var otherActor = new RefToken(RefTokenType.Subject, "456");
var content = CreateContent(Status.Published);
var command = CreateCommand(new DeleteContent { Actor = otherActor, User = otherUser });
GuardContent.CheckPermission(content, command, Permissions.AppContentsDelete);
}
[Fact]
public void CheckPermission_should_exception_if_content_is_from_another_user_and_user_has_no_permission()
{
var otherActor = new RefToken(RefTokenType.Subject, "456");
var content = CreateContent(Status.Published);
var command = CreateCommand(new DeleteContent { Actor = otherActor });
Assert.Throws<DomainForbiddenException>(() => GuardContent.CheckPermission(content, command, Permissions.AppContentsDelete));
}
private void SetupCanUpdate(bool canUpdate) private void SetupCanUpdate(bool canUpdate)
{ {
A.CallTo(() => contentWorkflow.CanUpdateAsync(A<IContentEntity>._, A<Status>._, user)) A.CallTo(() => contentWorkflow.CanUpdateAsync(A<IContentEntity>._, A<Status>._, user))
@ -336,24 +371,40 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
return Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), "my-schema"), new Schema("schema", isSingleton: isSingleton)); return Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), "my-schema"), new Schema("schema", isSingleton: isSingleton));
} }
private IContentEntity CreateDraftContent(Status status) private T CreateCommand<T>(T command) where T : ContentCommand
{ {
return new ContentEntity if (command.Actor == null)
{ {
Id = DomainId.NewGuid(), command.Actor = actor;
NewStatus = status, }
AppId = appId
}; if (command.User == null)
{
command.User = user;
}
return command;
}
private IContentEntity CreateDraftContent(Status status)
{
return CreateContentCore(new ContentEntity { NewStatus = status });
} }
private IContentEntity CreateContent(Status status) private IContentEntity CreateContent(Status status)
{ {
return new ContentEntity return CreateContentCore(new ContentEntity { Status = status });
}
private IContentEntity CreateContentCore(ContentEntity content)
{ {
Id = DomainId.NewGuid(), content.Id = DomainId.NewGuid();
Status = status, content.AppId = appId;
AppId = appId content.Created = default;
}; content.CreatedBy = actor;
content.SchemaId = schemaId;
return content;
} }
} }
} }

30
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

@ -218,6 +218,30 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.Empty(result); Assert.Empty(result);
} }
[Fact]
public async Task QueryAll_should_only_query_only_users_contents_if_no_permission()
{
var ctx =
CreateContext(true, true, Permissions.AppContentsReadOwn);
var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty);
A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, A<Q>.That.Matches(x => x.CreatedBy!.Equals(ctx.User.Token())), SearchScope.All))
.MustHaveHappened();
}
[Fact]
public async Task QueryAll_should_query_all_contents_if_user_has_permission()
{
var ctx =
CreateContext(true, true, Permissions.AppContentsRead);
var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty);
A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, A<Q>.That.Matches(x => x.CreatedBy == null), SearchScope.All))
.MustHaveHappened();
}
[Theory] [Theory]
[InlineData(1, 0, SearchScope.All)] [InlineData(1, 0, SearchScope.All)]
[InlineData(1, 1, SearchScope.All)] [InlineData(1, 1, SearchScope.All)]
@ -252,7 +276,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
}); });
} }
private Context CreateContext(bool isFrontend, bool allowSchema) private Context CreateContext(bool isFrontend, bool allowSchema, string permissionId = Permissions.AppContentsRead)
{ {
var claimsIdentity = new ClaimsIdentity(); var claimsIdentity = new ClaimsIdentity();
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
@ -264,9 +288,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (allowSchema) if (allowSchema)
{ {
var permission = Permissions.ForApp(Permissions.AppContentsRead, appId.Name, schemaId.Name).Id; var concretePermission = Permissions.ForApp(permissionId, appId.Name, schemaId.Name).Id;
claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, concretePermission));
} }
return new Context(claimsPrincipal, Mocks.App(appId)); return new Context(claimsPrincipal, Mocks.App(appId));

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs

@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
[Fact] [Fact]
public async Task Should_return_result_to_schema_and_contents_if_matching_and_permission_given() public async Task Should_return_result_to_schema_and_contents_if_matching_and_permission_given()
{ {
var permission = Permissions.ForApp(Permissions.AppContentsRead, appId.Name, "schemaA2"); var permission = Permissions.ForApp(Permissions.AppContentsReadOwn, appId.Name, "schemaA2");
var ctx = ContextWithPermission(permission.Id); var ctx = ContextWithPermission(permission.Id);
@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
[Fact] [Fact]
public async Task Should_return_result_to_schema_and_contents_if_schema_is_singleton() public async Task Should_return_result_to_schema_and_contents_if_schema_is_singleton()
{ {
var permission = Permissions.ForApp(Permissions.AppContentsRead, appId.Name, "schemaA1"); var permission = Permissions.ForApp(Permissions.AppContentsReadOwn, appId.Name, "schemaA1");
var ctx = ContextWithPermission(permission.Id); var ctx = ContextWithPermission(permission.Id);

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs

@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Shared.Identity;
namespace Squidex.Domain.Apps.Entities.TestHelpers namespace Squidex.Domain.Apps.Entities.TestHelpers
{ {
@ -55,12 +56,12 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
return CreateUser(role, "api"); return CreateUser(role, "api");
} }
public static ClaimsPrincipal FrontendUser(string? role = null) public static ClaimsPrincipal FrontendUser(string? role = null, string? permission = null)
{ {
return CreateUser(role, DefaultClients.Frontend); return CreateUser(role, DefaultClients.Frontend, permission);
} }
private static ClaimsPrincipal CreateUser(string? role, string client) private static ClaimsPrincipal CreateUser(string? role, string client, string? permission = null)
{ {
var claimsIdentity = new ClaimsIdentity(); var claimsIdentity = new ClaimsIdentity();
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
@ -72,6 +73,11 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
} }
if (permission != null)
{
claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission));
}
return claimsPrincipal; return claimsPrincipal;
} }
} }

Loading…
Cancel
Save