Browse Source

Permission fixes.

pull/636/head
Sebastian 5 years ago
parent
commit
dd6af0eb63
  1. 196
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  2. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs
  3. 290
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  4. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

196
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs

@ -16,44 +16,14 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
internal static class ContentActions
{
private static readonly QueryArgument Id = new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID).",
DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId
};
private static readonly QueryArgument NewId = new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The optional custom content id.",
DefaultValue = null,
ResolvedType = AllTypes.String
};
private static readonly QueryArgument ExpectedVersion = new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
};
private static readonly QueryArgument Publish = new QueryArgument(AllTypes.None)
{
Name = "publish",
Description = "Set to true to autopublish content on create.",
DefaultValue = false,
ResolvedType = AllTypes.Boolean
};
public static class Json
{
public static readonly QueryArguments Arguments = new QueryArguments
@ -95,7 +65,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public static readonly QueryArguments Arguments = new QueryArguments
{
Id,
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID).",
DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId
},
new QueryArgument(AllTypes.None)
{
Name = "version",
@ -191,11 +167,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType)
},
Publish, NewId
new QueryArgument(AllTypes.None)
{
Name = "publish",
Description = "Set to true to autopublish content on create.",
DefaultValue = false,
ResolvedType = AllTypes.Boolean
},
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The optional custom content id.",
DefaultValue = null,
ResolvedType = AllTypes.String
}
};
}
public static readonly IFieldResolver Resolver = ResolveAsync(c =>
public static readonly IFieldResolver Resolver = ResolveAsync(Permissions.AppContentsCreate, c =>
{
var contentPublish = c.GetArgument<bool>("publish");
var contentData = GetContentData(c);
@ -220,7 +209,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
return new QueryArguments
{
Id,
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID).",
DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId
},
new QueryArgument(AllTypes.None)
{
Name = "data",
@ -228,12 +223,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType)
},
Publish,
ExpectedVersion
new QueryArgument(AllTypes.None)
{
Name = "publish",
Description = "Set to true to autopublish content on create.",
DefaultValue = false,
ResolvedType = AllTypes.Boolean
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
};
}
public static readonly IFieldResolver Resolver = ResolveAsync(c =>
public static readonly IFieldResolver Resolver = ResolveAsync(Permissions.AppContentsUpsert, c =>
{
var contentPublish = c.GetArgument<bool>("publish");
var contentData = GetContentData(c);
@ -251,7 +258,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
return new QueryArguments
{
Id,
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The optional custom content id.",
DefaultValue = null,
ResolvedType = AllTypes.String
},
new QueryArgument(AllTypes.None)
{
Name = "data",
@ -259,11 +272,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType)
},
ExpectedVersion
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
};
}
public static readonly IFieldResolver Resolver = ResolveAsync(c =>
public static readonly IFieldResolver Resolver = ResolveAsync(Permissions.AppContentsUpdateOwn, c =>
{
var contentId = c.GetArgument<DomainId>("id");
var contentData = GetContentData(c);
@ -278,7 +297,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
return new QueryArguments
{
Id,
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The optional custom content id.",
DefaultValue = null,
ResolvedType = AllTypes.String
},
new QueryArgument(AllTypes.None)
{
Name = "data",
@ -286,11 +311,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType)
},
ExpectedVersion
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
};
}
public static readonly IFieldResolver Resolver = ResolveAsync(c =>
public static readonly IFieldResolver Resolver = ResolveAsync(Permissions.AppContentsUpdateOwn, c =>
{
var contentId = c.GetArgument<DomainId>("id");
var contentData = GetContentData(c);
@ -303,7 +334,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public static readonly QueryArguments Arguments = new QueryArguments
{
Id,
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID).",
DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId
},
new QueryArgument(AllTypes.None)
{
Name = "status",
@ -318,10 +355,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
DefaultValue = null,
ResolvedType = AllTypes.Date
},
ExpectedVersion
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
};
public static readonly IFieldResolver Resolver = ResolveAsync(c =>
public static readonly IFieldResolver Resolver = ResolveAsync(Permissions.AppContentsChangeStatusOwn, c =>
{
var contentId = c.GetArgument<DomainId>("id");
var contentStatus = c.GetArgument<Status>("status");
@ -335,11 +378,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public static readonly QueryArguments Arguments = new QueryArguments
{
Id,
ExpectedVersion
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID).",
DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
};
public static readonly IFieldResolver Resolver = ResolveAsync(c =>
public static readonly IFieldResolver Resolver = ResolveAsync(Permissions.AppContentsDeleteOwn, c =>
{
var contentId = c.GetArgument<DomainId>("id");
@ -354,35 +409,34 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
return source.ToContentData((IComplexGraphType)c.FieldDefinition.Arguments.Find("data").Flatten());
}
private static IFieldResolver ResolveAsync(Func<IResolveFieldContext, ContentCommand> action)
private static IFieldResolver ResolveAsync(string permissionId, Func<IResolveFieldContext, ContentCommand> action)
{
return Resolvers.Async<object, object>(async (source, fieldContext, context) =>
{
try
{
var command = action(fieldContext);
var schemaId = fieldContext.FieldDefinition.SchemaNamedId();
command.AppId = fieldContext.FieldDefinition.AppId();
command.SchemaId = fieldContext.FieldDefinition.SchemaNamedId();
command.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any);
CheckPermission(permissionId, context, schemaId);
var commandContext = await context.CommandBus.PublishAsync(command);
var contentCommand = action(fieldContext);
return commandContext.PlainResult!;
}
catch (ValidationException ex)
{
fieldContext.Errors.Add(new ExecutionError(ex.Message));
contentCommand.SchemaId = schemaId;
contentCommand.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any);
throw;
}
catch (DomainException ex)
{
fieldContext.Errors.Add(new ExecutionError(ex.Message));
var commandContext = await context.CommandBus.PublishAsync(contentCommand);
throw;
}
return commandContext.PlainResult!;
});
}
private static void CheckPermission(string permissionId, GraphQLExecutionContext context, NamedId<DomainId> schemaId)
{
var requestContext = context.Context;
var requestPermission = Permissions.ForApp(permissionId, requestContext.App.Name, schemaId.Name);
if (!requestContext.Permissions.Allows(requestPermission))
{
throw new DomainForbiddenException(T.Get("common.errorNoPermission"));
}
}
}
}

10
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs

@ -88,16 +88,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return field.GetMetadata<NamedId<DomainId>>(nameof(SchemaNamedId));
}
public static FieldType WithAppId(this FieldType field, NamedId<DomainId> value)
{
return field.WithMetadata(nameof(AppId), value);
}
public static NamedId<DomainId> AppId(this FieldType field)
{
return field.GetMetadata<NamedId<DomainId>>(nameof(AppId));
}
private static FieldType WithMetadata(this FieldType field, string key, object value)
{
if (field is MetadataProvider metadataProvider)

290
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs

@ -16,8 +16,10 @@ using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
@ -37,10 +39,49 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
[Fact]
public async Task Should_return_single_content_when_creating_content()
public async Task Should_return_error_when_user_has_no_permission_to_create()
{
var f = new FloatGraphType().ParseValue("12.0");
var query = @"
mutation {
createMySchemaContent(data: { myNumber: { iv: 42 } }) {
id
}
}";
var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn);
var expected = new
{
data = new
{
createMySchemaContent = (object?)null,
},
errors = new[]
{
new
{
message = "You do not have the necessary permission.",
locations = new[]
{
new
{
line = 3,
column = 19
}
}
}
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(A<ICommand>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_return_single_content_when_creating_content()
{
var query = @"
mutation {
createMySchemaContent(data: <DATA>, publish: true) {
@ -50,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsCreate);
var expected = new
{
@ -83,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsCreate);
var expected = new
{
@ -117,7 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, Inputs = GetInput() });
var result = await ExecuteAsync( new GraphQLQuery { Query = query, Inputs = GetInput() }, Permissions.AppContentsCreate);
var expected = new
{
@ -138,6 +179,47 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
.MustHaveHappened();
}
[Fact]
public async Task Should_return_error_when_user_has_no_permission_to_update()
{
var query = @"
mutation {
updateMySchemaContent(id: ""<ID>"", data: { myNumber: { iv: 42 } }) {
id
}
}".Replace("<ID>", contentId.ToString());
var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn);
var expected = new
{
data = new
{
updateMySchemaContent = (object?)null,
},
errors = new[]
{
new
{
message = "You do not have the necessary permission.",
locations = new[]
{
new
{
line = 3,
column = 19
}
}
}
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(A<ICommand>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_return_single_content_when_updating_content()
{
@ -150,7 +232,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsUpdateOwn);
var expected = new
{
@ -182,7 +264,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, Inputs = GetInput() });
var result = await ExecuteAsync( new GraphQLQuery { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpdateOwn);
var expected = new
{
@ -202,6 +284,47 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
.MustHaveHappened();
}
[Fact]
public async Task Should_return_error_when_user_has_no_permission_to_upsert()
{
var query = @"
mutation {
upsertMySchemaContent(id: ""<ID>"", data: { myNumber: { iv: 42 } }) {
id
}
}".Replace("<ID>", contentId.ToString());
var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn);
var expected = new
{
data = new
{
upsertMySchemaContent = (object?)null,
},
errors = new[]
{
new
{
message = "You do not have the necessary permission.",
locations = new[]
{
new
{
line = 3,
column = 19
}
}
}
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(A<ICommand>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_return_single_content_when_upserting_content()
{
@ -214,7 +337,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsUpsert);
var expected = new
{
@ -247,7 +370,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, Inputs = GetInput() });
var result = await ExecuteAsync( new GraphQLQuery { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpsert);
var expected = new
{
@ -268,6 +391,47 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
.MustHaveHappened();
}
[Fact]
public async Task Should_return_error_when_user_has_no_permission_to_patch()
{
var query = @"
mutation {
patchMySchemaContent(id: ""<ID>"", data: { myNumber: { iv: 42 } }) {
id
}
}".Replace("<ID>", contentId.ToString());
var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn);
var expected = new
{
data = new
{
patchMySchemaContent = (object?)null,
},
errors = new[]
{
new
{
message = "You do not have the necessary permission.",
locations = new[]
{
new
{
line = 3,
column = 19
}
}
}
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(A<ICommand>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_return_single_content_when_patching_content()
{
@ -280,7 +444,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsUpdateOwn);
var expected = new
{
@ -312,7 +476,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, Inputs = GetInput() });
var result = await ExecuteAsync( new GraphQLQuery { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpdateOwn);
var expected = new
{
@ -333,7 +497,48 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
[Fact]
public async Task Should_publish_command_for_status_change()
public async Task Should_return_error_when_user_has_no_permission_to_change_status()
{
var query = @"
mutation {
changeMySchemaContent(id: ""<ID>"", status: ""Published"") {
id
}
}".Replace("<ID>", contentId.ToString());
var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn);
var expected = new
{
data = new
{
changeMySchemaContent = (object?)null,
},
errors = new[]
{
new
{
message = "You do not have the necessary permission.",
locations = new[]
{
new
{
line = 3,
column = 19
}
}
}
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(A<ICommand>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_return_single_content_when_changing_status()
{
var dueTime = SystemClock.Instance.GetCurrentInstant().WithoutMs();
@ -346,7 +551,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsChangeStatusOwn);
var expected = new
{
@ -368,7 +573,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
[Fact]
public async Task Should_publish_command_for_status_change_without_due_time()
public async Task Should_return_single_content_when_changing_status_without_due_time()
{
var query = @"
mutation {
@ -379,7 +584,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsChangeStatusOwn);
var expected = new
{
@ -401,7 +606,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
[Fact]
public async Task Should_publish_command_for_status_change_with_null_due_time()
public async Task Should_return_single_content_when_changing_status_with_null_due_time()
{
var query = @"
mutation {
@ -412,7 +617,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsChangeStatusOwn);
var expected = new
{
@ -434,7 +639,45 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
[Fact]
public async Task Should_publish_command_for_delete()
public async Task Should_return_error_when_user_has_no_permission_to_delete()
{
var query = @"
mutation {
deleteMySchemaContent(id: ""<ID>"") {
version
}
}".Replace("<ID>", contentId.ToString());
var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn);
var expected = new
{
data = (object?)null,
errors = new[]
{
new
{
message = "You do not have the necessary permission.",
locations = new[]
{
new
{
line = 3,
column = 19
}
}
}
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(A<ICommand>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_return_new_version_when_deleting_content()
{
var query = @"
mutation {
@ -445,7 +688,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(new EntitySavedResult(13));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsDeleteOwn);
var expected = new
{
@ -467,6 +710,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
.MustHaveHappened();
}
private Task<(bool HasError, object Response)> ExecuteAsync(GraphQLQuery query, string permissionId)
{
var permission = Permissions.ForApp(permissionId, app.Name, schemaId.Name).Id;
var withPermission = new Context(Mocks.FrontendUser(permission: permission), app);
return sut.QueryAsync(withPermission, query);
}
private Inputs GetInput()
{
var input = new

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

@ -33,7 +33,7 @@ using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public class GraphQLTestBase
public class GraphQLTestBase : IClassFixture<TranslationsFixture>
{
protected readonly IAppEntity app;
protected readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();

Loading…
Cancel
Save