Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A Schema First example that uses Authorization #66

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/SchemaFirstAuthorization/Example/Example.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GraphQL.Authorization" Version="4.0.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="4.5.0" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="5.0.2" />
</ItemGroup>

</Project>
22 changes: 22 additions & 0 deletions src/SchemaFirstAuthorization/Example/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Example
{
public static class Extensions
{
public static List<Type> WithAttribute<T>(this Assembly assembly)
where T : Attribute
{
return assembly.GetTypes().Where(x => x.GetCustomAttributes<T>().Length > 0).ToList();
}

public static T[] GetCustomAttributes<T>(this Type type)
where T : Attribute
{
return type.GetCustomAttributes(typeof(T), true).OfType<T>().ToArray();
}
}
}
34 changes: 34 additions & 0 deletions src/SchemaFirstAuthorization/Example/GraphQL.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http;

namespace Example
{
public class GraphQLUserContext : Dictionary<string, object>
{
public ClaimsPrincipal User { get; set; }
}

public class GraphQLRequest
{
[JsonPropertyName("operationName")]
public string OperationName { get; set; }

[JsonPropertyName("query")]
public string Query { get; set; }

[JsonPropertyName("variables")]
public GraphQL.Inputs Variables { get; set; }
}

public class GraphQLSettings
{
public PathString Path { get; set; } = "/api/graphql";

public Func<HttpContext, IDictionary<string, object>> BuildUserContext { get; set; }

public bool EnableMetrics { get; set; }
}
}
86 changes: 86 additions & 0 deletions src/SchemaFirstAuthorization/Example/GraphQLMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using GraphQL;
using GraphQL.Authorization;
using GraphQL.SystemTextJson;
using GraphQL.Types;
using GraphQL.Validation;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;

namespace Example
{
public class GraphQLMiddleware
{
private readonly RequestDelegate _next;
private readonly GraphQLSettings _settings;
private readonly IDocumentExecuter _executer;
private readonly IDocumentWriter _writer;
private readonly JsonSerializerOptions _serializerOptions;

public GraphQLMiddleware(
RequestDelegate next,
GraphQLSettings settings,
IDocumentExecuter executer,
IDocumentWriter writer)
{
_next = next;
_settings = settings;
_executer = executer;
_writer = writer;

_serializerOptions = new JsonSerializerOptions();
_serializerOptions.Converters.Add(new InputsConverter());
}

public async Task Invoke(HttpContext context, ISchema schema, IEnumerable<IValidationRule> rules)
{
if (!IsGraphQLRequest(context))
{
await _next(context);
return;
}

await ExecuteAsync(context, schema, rules);
}

private bool IsGraphQLRequest(HttpContext context)
{
return context.Request.Path.StartsWithSegments(_settings.Path)
&& string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase);
}

private async Task ExecuteAsync(HttpContext context, ISchema schema, IEnumerable<IValidationRule> rules)
{
var request = await JsonSerializer.DeserializeAsync<GraphQLRequest>(context.Request.BodyReader.AsStream(), _serializerOptions);

var result = await _executer.ExecuteAsync(_ =>
{
_.Schema = schema;
_.Query = request?.Query;
_.OperationName = request?.OperationName;
_.Inputs = request?.Variables;
_.UserContext = _settings.BuildUserContext?.Invoke(context);
_.ValidationRules = DocumentValidator.CoreRules.Concat(rules);
_.EnableMetrics = _settings.EnableMetrics;
_.UnhandledExceptionDelegate = context =>
{
System.Diagnostics.Debug.WriteLine(context.OriginalException.Message);
};
});

await WriteResponseAsync(context, result);
}

private async Task WriteResponseAsync(HttpContext context, ExecutionResult result)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = result.Errors?.Any() == true ? (int)HttpStatusCode.BadRequest : (int)HttpStatusCode.OK;

await _writer.WriteAsync(context.Response.Body, result);
}
}
}
26 changes: 26 additions & 0 deletions src/SchemaFirstAuthorization/Example/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Example
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:39369",
"sslPort": 44370
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Example": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
33 changes: 33 additions & 0 deletions src/SchemaFirstAuthorization/Example/Requirements.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Threading.Tasks;
using GraphQL.Authorization;
using Microsoft.AspNetCore.Http;

namespace Example
{
/// <summary>
/// This represents a custom Authorization Requirement. These can be used
/// to get data from an external source (such as a database) or check
/// information that isn't directly available in the ClaimsPrincipal.
///
/// For example, you can use IHttpContextAccessor to check data on the HttpContext.
/// </summary>
public class UserIsJoeRequirement : IAuthorizationRequirement
{
private readonly IHttpContextAccessor _accessor;

public UserIsJoeRequirement(IHttpContextAccessor accessor)
{
_accessor = accessor;
}

public Task Authorize(AuthorizationContext context)
{
// this is the same as context.User
if (_accessor.HttpContext.User.Identity?.Name != "Joe")
{
context.ReportError("User is not Joe!");
}
return Task.CompletedTask;
}
}
}
42 changes: 42 additions & 0 deletions src/SchemaFirstAuthorization/Example/Resolvers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using GraphQL;

namespace Example
{
public class GraphTypeMetadataAttribute : Attribute
{
public GraphTypeMetadataAttribute(string typeDef)
{
TypeDef = typeDef;
}

public string TypeDef { get; }
}

[GraphTypeMetadata(@"
type User {
id: ID
name: String
}
")]
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}

[GraphQLMetadata("Query")]
[GraphTypeMetadata(@"
extend type Query {
viewer: User
}
")]
public class QueryType
{
[GraphQLAuthorize("CustomRequirement")]
public User Viewer()
{
return new User { Id = 1, Name = "Quinn" };
}
}
}
93 changes: 93 additions & 0 deletions src/SchemaFirstAuthorization/Example/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Linq;
using GraphQL;
using GraphQL.Authorization;
using GraphQL.Server.Ui.Playground;
using GraphQL.Types;
using GraphQL.Validation;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Example
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization();
services.AddHttpContextAccessor();

services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
services.AddSingleton<IDocumentWriter, GraphQL.SystemTextJson.DocumentWriter>();

services.AddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.AddTransient<UserIsJoeRequirement>();

services.AddTransient(s =>
{
var authSettings = new AuthorizationSettings();
// add a policy that checks claims on the Authenticated User
authSettings.AddPolicy("AdminPolicy", p => p.RequireClaim("role", "Admin"));

// add a policy which uses a custom AuthorizationRequirement
authSettings.AddPolicy("CustomRequirement", p => p.AddRequirement(s.GetRequiredService<UserIsJoeRequirement>()));
return authSettings;
});

services.AddSingleton<ISchema>(s =>
{
// get all types in assembly that have the GraphTypeMetadataAttribute attribute
// use the schema-first type definitions to build the schema
var types = typeof(QueryType).Assembly.WithAttribute<GraphTypeMetadataAttribute>();
var typeDefs = types
.SelectMany(x => x.GetCustomAttributes<GraphTypeMetadataAttribute>())
.Select(x => x.TypeDef)
.ToArray();

var schemaFirst = string.Join(Environment.NewLine, typeDefs);

return Schema.For(schemaFirst, _ =>
{
foreach (var type in types)
{
_.Types.Include(type);
}
});
});
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();
app.UseAuthorization();

app.UseGraphQLPlayground(new PlaygroundOptions { GraphQLEndPoint = "/api/graphql" });

app.UseMiddleware<GraphQLMiddleware>(new GraphQLSettings
{
Path = "/api/graphql",
BuildUserContext = ctx => new GraphQLUserContext
{
User = ctx.User
},
EnableMetrics = true
});
}
}
}
Loading