diff --git a/Demo/Security/CommandAndQueryAuthorizationService.cs b/Demo/Security/CommandAndQueryAuthorizationService.cs new file mode 100644 index 0000000..bc83625 --- /dev/null +++ b/Demo/Security/CommandAndQueryAuthorizationService.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Http; +using PoweredSoft.CQRS.Abstractions.Security; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Demo.Security +{ + public class CommandAndQueryAuthorizationService : IQueryAuthorizationService, ICommandAuthorizationService + { + private readonly IHttpContextAccessor httpContextAccessor; + + public CommandAndQueryAuthorizationService(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + public Task IsAllowedAsync(Type queryOrCommandType, CancellationToken cancellationToken = default) + { + var authResult = httpContextAccessor.HttpContext.Request.Query["auth-result"].FirstOrDefault(); + if (authResult == "Unauthorized") + return Task.FromResult(AuthorizationResult.Unauthorized); + else if (authResult == "Forbidden") + return Task.FromResult(AuthorizationResult.Forbidden); + + return Task.FromResult(AuthorizationResult.Allowed); + } + } +} diff --git a/Demo/Startup.cs b/Demo/Startup.cs index a7a97ff..9612d32 100644 --- a/Demo/Startup.cs +++ b/Demo/Startup.cs @@ -22,6 +22,8 @@ using PoweredSoft.Data.Core; using PoweredSoft.DynamicQuery; using System.Linq; using PoweredSoft.CQRS.GraphQL.HotChocolate.DynamicQuery; +using PoweredSoft.CQRS.Abstractions.Security; +using Demo.Security; namespace Demo { @@ -41,6 +43,9 @@ namespace Demo AddDynamicQueries(services); AddCommands(services); + services.AddHttpContextAccessor(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddPoweredSoftDataServices(); services.AddPoweredSoftDynamicQuery(); diff --git a/PoweredSoft.CQRS.Abstractions/Security/AuthorizationResult.cs b/PoweredSoft.CQRS.Abstractions/Security/AuthorizationResult.cs new file mode 100644 index 0000000..7520d88 --- /dev/null +++ b/PoweredSoft.CQRS.Abstractions/Security/AuthorizationResult.cs @@ -0,0 +1,9 @@ +namespace PoweredSoft.CQRS.Abstractions.Security +{ + public enum AuthorizationResult + { + Unauthorized, + Forbidden, + Allowed + } +} diff --git a/PoweredSoft.CQRS.Abstractions/Security/ICommandAuthorizationService.cs b/PoweredSoft.CQRS.Abstractions/Security/ICommandAuthorizationService.cs new file mode 100644 index 0000000..fea7568 --- /dev/null +++ b/PoweredSoft.CQRS.Abstractions/Security/ICommandAuthorizationService.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PoweredSoft.CQRS.Abstractions.Security +{ + public interface ICommandAuthorizationService + { + Task IsAllowedAsync(Type commandType, CancellationToken cancellationToken = default); + } +} diff --git a/PoweredSoft.CQRS.Abstractions/Security/IQueryAuthorizationService.cs b/PoweredSoft.CQRS.Abstractions/Security/IQueryAuthorizationService.cs new file mode 100644 index 0000000..a4d1d55 --- /dev/null +++ b/PoweredSoft.CQRS.Abstractions/Security/IQueryAuthorizationService.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PoweredSoft.CQRS.Abstractions.Security +{ + + public interface IQueryAuthorizationService + { + Task IsAllowedAsync(Type queryType, CancellationToken cancellationToken = default); + } +} diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate.DynamicQuery/DynamicQueryObjectType.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate.DynamicQuery/DynamicQueryObjectType.cs index 5519d72..192bca0 100644 --- a/PoweredSoft.CQRS.GraphQL.HotChocolate.DynamicQuery/DynamicQueryObjectType.cs +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate.DynamicQuery/DynamicQueryObjectType.cs @@ -61,6 +61,9 @@ namespace PoweredSoft.CQRS.GraphQL.HotChocolate.DynamicQuery f.Type(resultType); + // security middleware + f.Use((sp, d) => new QueryAuthorizationMiddleware(q.QueryType, d)); + // middleware to validate. f.Use(); diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationAuthorizationMiddleware.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationAuthorizationMiddleware.cs new file mode 100644 index 0000000..afc2cfa --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationAuthorizationMiddleware.cs @@ -0,0 +1,46 @@ +using HotChocolate; +using HotChocolate.Resolvers; +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using PoweredSoft.CQRS.Abstractions.Security; + +namespace PoweredSoft.CQRS.GraphQL.HotChocolate +{ + internal class MutationAuthorizationMiddleware + { + private readonly Type mutationType; + private readonly FieldDelegate _next; + + public MutationAuthorizationMiddleware(Type mutationType,FieldDelegate next) + { + this.mutationType = mutationType; + _next = next; + } + + public async Task InvokeAsync(IMiddlewareContext context) + { + var mutationAuthorizationService = context.Service().GetService(); + if (mutationAuthorizationService != null) + { + var authorizationResult = await mutationAuthorizationService.IsAllowedAsync(mutationType); + if (authorizationResult != AuthorizationResult.Allowed) + { + var eb = ErrorBuilder.New() + .SetMessage(authorizationResult == AuthorizationResult.Unauthorized ? "Unauthorized" : "Forbidden") + .SetCode("AuthorizationResult") + .SetExtension("StatusCode", authorizationResult == AuthorizationResult.Unauthorized ? "401" : "403") + .SetPath(context.Path) + .AddLocation(context.Selection.SyntaxNode); + + context.Result = eb.Build(); + + return; + } + } + + + await _next.Invoke(context); + } + } +} \ No newline at end of file diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryAuthorizationMiddleware.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryAuthorizationMiddleware.cs new file mode 100644 index 0000000..f1cf2e3 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryAuthorizationMiddleware.cs @@ -0,0 +1,46 @@ +using HotChocolate; +using HotChocolate.Resolvers; +using Microsoft.Extensions.DependencyInjection; +using PoweredSoft.CQRS.Abstractions.Security; +using System; +using System.Threading.Tasks; + +namespace PoweredSoft.CQRS.GraphQL.HotChocolate +{ + public class QueryAuthorizationMiddleware + { + private readonly Type queryType; + private readonly FieldDelegate _next; + + public QueryAuthorizationMiddleware(Type queryType, FieldDelegate next) + { + this.queryType = queryType; + _next = next; + } + + public async Task InvokeAsync(IMiddlewareContext context) + { + var queryAuthorizationService = context.Service().GetService(); + if (queryAuthorizationService != null) + { + var authorizationResult = await queryAuthorizationService.IsAllowedAsync(queryType); + if (authorizationResult != AuthorizationResult.Allowed) + { + var eb = ErrorBuilder.New() + .SetMessage(authorizationResult == AuthorizationResult.Unauthorized ? "Unauthorized" : "Forbidden") + .SetCode("AuthorizationResult") + .SetExtension("StatusCode", authorizationResult == AuthorizationResult.Unauthorized ? "401" : "403") + .SetPath(context.Path) + .AddLocation(context.Selection.SyntaxNode); + + context.Result = eb.Build(); + + return; + } + } + + + await _next.Invoke(context); + } + } +} \ No newline at end of file diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs index 386e0c1..b8db845 100644 --- a/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs @@ -4,6 +4,8 @@ using HotChocolate.Types; using PoweredSoft.CQRS.Abstractions; using PoweredSoft.CQRS.Abstractions.Discovery; using System; +using System.Collections.Generic; +using System.Linq; using System.Text; namespace PoweredSoft.CQRS.GraphQL.HotChocolate @@ -28,11 +30,25 @@ namespace PoweredSoft.CQRS.GraphQL.HotChocolate var queryField = desc.Field(q.LowerCamelCaseName); var typeToGet = typeof(IQueryHandler<,>).MakeGenericType(q.QueryType, q.QueryResultType); - queryField.Type(q.QueryResultType); + queryField.Use((sp, d) => new QueryAuthorizationMiddleware(q.QueryType, d)); - // TODO. - // always required. - //queryField.Use((sp, d) => new QueryAuthorizationMiddleware(q.QueryType, d)); + // if its a IQueryable. + if (q.QueryResultType.Namespace == "System.Linq" && q.QueryResultType.Name.Contains("IQueryable")) + { + //waiting on answer to be determined. + /*var genericArgument = q.QueryResultType.GetGenericArguments().First(); + var type = new ListType(new NonNullType(new NamedTypeNode)); + queryField.Type(type); + queryField.UsePaging(); + */ + + queryField.Type(q.QueryResultType); + } + else + { + queryField.Type(q.QueryResultType); + + } if (q.QueryType.GetProperties().Length == 0) {