Skip to content

Commit

Permalink
Merge pull request #219 from dolittle:authorization
Browse files Browse the repository at this point in the history
authorization
  • Loading branch information
einari authored May 11, 2021
2 parents e89ccc5 + 30a5896 commit c34c0ad
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 3 deletions.
64 changes: 64 additions & 0 deletions Documentation/backend/DotNET/authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Authorization

Authorization is done by leveraging the [ASP.NET `[Authorize]`](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-5.0)
attribute. [Hot Chocolate GraphQL](https://chillicream.com/docs/hotchocolate/v10/server/#supported-core-components) is automatically hooked up for this.

## Configuration

With the ASP.NET Core policy system, you simply configure authorization and adds your policies:

```csharp

public class Startup
{
public void ConfigureService(IServiceCollection services)
{
services.AddAuthorization(options => options
.AddPolicy("MyPolicy", policy => policy.RequireClaim("Admin")));

services.AddVanir();
}
}
```

## Mutations / Queries

Once you have it configured, you can simply start leveraging the `[Authorize]` attribute and reference the required
policy or policies. This can be done either on a class level and then automatically apply to all mutations or queries within
the class, or you can set it specifically for the methods.

> Note: If you add an authorization policy on a class level, any method policies will be in addition to the class level one.
Class level:

```csharp
using Microsoft.AspNetCore.Authorization;
using Dolittle.Vanir.Backend.GraphQL;

[Authorize(Policy = "MyPolicy")]
public class MyMutations : GraphController
{
[Mutation]
public async Task<bool> DoSomething()
{

}
}
```

Method level:

```csharp
using Microsoft.AspNetCore.Authorization;
using Dolittle.Vanir.Backend.GraphQL;

public class MyMutations : GraphController
{
[Mutation]
[Authorize(Policy = "MyPolicy")]
public async Task<bool> DoSomething()
{

}
}
```
3 changes: 2 additions & 1 deletion Documentation/backend/DotNET/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@

| Title | Description |
| ----- | ----------- |
| [Validation](./validation.md) | How validation for your backend |
| [Validation](./validation.md) | How validation works |
| [Authorization](./authorization.md) | How to enable authorization |
20 changes: 20 additions & 0 deletions Samples/Source/Aspnetcore/Backend/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Autofac;
using Dolittle.SDK;
using HotChocolate.Execution.Configuration;
using HotChocolate.Types;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -11,6 +15,18 @@

namespace Backend
{
public class MyPolicy : IAuthorizationService
{
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
return Task.FromResult(AuthorizationResult.Failed());
}

public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
{
return Task.FromResult(AuthorizationResult.Failed());
}
}

public class Startup
{
Expand All @@ -21,6 +37,10 @@ public void ConfigureServices(IServiceCollection services)
var loggerFactory = new LoggerFactory();
loggerFactory.AddSerilog(Log.Logger);

services.AddAuthorization(options => options
.AddPolicy("MyPolicy", policy =>
policy.RequireClaim("Admin")));

services.AddVanir(new()
{
LoggerFactory = loggerFactory,
Expand Down
1 change: 1 addition & 0 deletions Samples/Source/Aspnetcore/Backend/Things.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Dolittle.Vanir.Backend.Execution;
using Dolittle.Vanir.Backend.GraphQL;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Reflection;
using Dolittle.Vanir.Backend.GraphQL;
using HotChocolate;
using HotChocolate.Types;
using Machine.Specifications;
using Moq;
using static Moq.It;

namespace Dolittle.Vanir.Backend.Specs.GraphQL.for_SchemaRoute.given
{
public class an_empty_schema_route
{
protected static string path = "SomePath";
protected static string local_name = "LocalName";
protected static string type_name = "TypeName";
protected static SchemaRoute route;
protected static Mock<IObjectTypeDescriptor> object_type_descriptor;
protected static Mock<IObjectFieldDescriptor> object_field_descriptor;

static MethodInfo configure_method;

Establish context = () =>
{
route = new SchemaRoute(path, local_name, type_name);
configure_method = typeof(SchemaRoute).GetMethod("Configure", BindingFlags.Instance | BindingFlags.NonPublic);
object_type_descriptor = new();
object_field_descriptor = new();
object_type_descriptor.Setup(_ => _.Field(IsAny<NameString>())).Returns(object_field_descriptor.Object);
object_type_descriptor.Setup(_ => _.Field(IsAny<MethodInfo>())).Returns(object_field_descriptor.Object);
object_field_descriptor.Setup(_ => _.Type(IsAny<SchemaRoute>())).Returns(object_field_descriptor.Object);
object_field_descriptor.Setup(_ => _.Name(IsAny<NameString>())).Returns(object_field_descriptor.Object);
};

protected static void Configure(IObjectTypeDescriptor descriptor = default)
{
if (descriptor == default) descriptor = object_type_descriptor.Object;
configure_method.Invoke(route, new[] { descriptor });
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Dolittle.Vanir.Backend.GraphQL;
using Machine.Specifications;
using static Moq.Times;

namespace Dolittle.Vanir.Backend.Specs.GraphQL.for_SchemaRoute.when_configuring
{
public class and_there_are_children : given.an_empty_schema_route
{
static SchemaRoute first_child;
static SchemaRoute second_child;

Establish context = () =>
{
first_child = new SchemaRoute("FirstChildPath", "FirstChildLocalName", "FirstChildTypeName");
second_child = new SchemaRoute("SecondChildPath", "SecondChildLocalName", "SecondChildTypeName");
route.AddChild(first_child);
route.AddChild(second_child);
};
Because of = () => Configure();

It should_add_a_field_for_first_child = () => object_type_descriptor.Verify(_ => _.Field(first_child.LocalName), Once());
It should_add_a_field_for_second_child = () => object_type_descriptor.Verify(_ => _.Field(second_child.LocalName), Once());
It should_set_type_for_first_child = () => object_field_descriptor.Verify(_ => _.Type(first_child), Once());
It should_set_type_for_second_child = () => object_field_descriptor.Verify(_ => _.Type(second_child), Once());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Reflection;
using Dolittle.Vanir.Backend.GraphQL;
using Machine.Specifications;
using static Moq.Times;

namespace Dolittle.Vanir.Backend.Specs.GraphQL.for_SchemaRoute.when_configuring
{
public class and_there_are_items : given.an_empty_schema_route
{
class ClassWithMethods
{
public void FirstMethod() { }
public void SecondMethod() { }
}

static SchemaRouteItem first_item;
static SchemaRouteItem second_item;

Establish context = () =>
{
var methods = typeof(ClassWithMethods).GetMethods(BindingFlags.Instance|BindingFlags.Public);
first_item = new SchemaRouteItem(methods[0], "FirstItem");
route.AddItem(first_item);
second_item = new SchemaRouteItem(methods[1], "SecondItem");
route.AddItem(second_item);
};

Because of = () => Configure();

It should_add_field_for_first_method = () => object_type_descriptor.Verify(_ => _.Field(first_item.Method), Once());
It should_add_field_for_second_method = () => object_type_descriptor.Verify(_ => _.Field(second_item.Method), Once());
It should_set_name_for_first_method = () => object_field_descriptor.Verify(_ => _.Name(first_item.Name), Once());
It should_set_name_for_second_method = () => object_field_descriptor.Verify(_ => _.Name(second_item.Name), Once());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Reflection;
using Dolittle.Vanir.Backend.GraphQL;
using HotChocolate;
using HotChocolate.AspNetCore.Authorization;
using HotChocolate.Types;
using Machine.Specifications;
using Moq;
using static Moq.It;
using static Moq.Times;
using AuthorizeAttribute = Microsoft.AspNetCore.Authorization.AuthorizeAttribute;
using It = Machine.Specifications.It;

namespace Dolittle.Vanir.Backend.Specs.GraphQL.for_SchemaRoute.when_configuring
{
public class and_there_are_items_with_authorization_on_class_and_methods : given.an_empty_schema_route
{
[Authorize(Policy = ClassPolicy)]
class ClassWithMethods
{
public const string ClassPolicy = "ClassPolicy";
public const string FirstMethodPolicy = "FirstMethodPolicy";
public const string SecondMethodPolicy = "SecondMethodPolicy";

[Authorize(Policy = FirstMethodPolicy)]
public void FirstMethod() { }

[Authorize(Policy = SecondMethodPolicy)]
public void SecondMethod() { }
}

static SchemaRouteItem first_item;
static SchemaRouteItem second_item;

static Mock<IObjectFieldDescriptor> first_method_descriptor;
static Mock<IObjectFieldDescriptor> second_method_descriptor;

Establish context = () =>
{
var methods = typeof(ClassWithMethods).GetMethods(BindingFlags.Instance | BindingFlags.Public);
first_item = new SchemaRouteItem(methods[0], "FirstItem");
route.AddItem(first_item);
second_item = new SchemaRouteItem(methods[1], "SecondItem");
route.AddItem(second_item);
first_method_descriptor = new();
first_method_descriptor.Setup(_ => _.Type(IsAny<SchemaRoute>())).Returns(first_method_descriptor.Object);
first_method_descriptor.Setup(_ => _.Name(IsAny<NameString>())).Returns(first_method_descriptor.Object);
second_method_descriptor = new();
second_method_descriptor.Setup(_ => _.Type(IsAny<SchemaRoute>())).Returns(second_method_descriptor.Object);
second_method_descriptor.Setup(_ => _.Name(IsAny<NameString>())).Returns(second_method_descriptor.Object);
object_type_descriptor.Setup(_ => _.Field(first_item.Method)).Returns(first_method_descriptor.Object);
object_type_descriptor.Setup(_ => _.Field(second_item.Method)).Returns(second_method_descriptor.Object);
object_type_descriptor.Setup(_ => _.Directive(IsAny<AuthorizeDirective>())).Returns(object_type_descriptor.Object);
};

Because of = () => Configure();

It should_set_method_authorization_for_first_method = () => first_method_descriptor.Verify(_ => _.Directive(Is<AuthorizeDirective>(a => a.Policy == ClassWithMethods.FirstMethodPolicy)), Once());
It should_set_method_authorization_for_second_method = () => second_method_descriptor.Verify(_ => _.Directive(Is<AuthorizeDirective>(a => a.Policy == ClassWithMethods.SecondMethodPolicy)), Once());
It should_set_class_authorization_for_first_method = () => first_method_descriptor.Verify(_ => _.Directive(Is<AuthorizeDirective>(a => a.Policy == ClassWithMethods.ClassPolicy)), Once());
It should_set_class_authorization_for_second_method = () => second_method_descriptor.Verify(_ => _.Directive(Is<AuthorizeDirective>(a => a.Policy == ClassWithMethods.ClassPolicy)), Once());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Machine.Specifications;
using static Moq.Times;

namespace Dolittle.Vanir.Backend.Specs.GraphQL.for_SchemaRoute.when_configuring
{
public class and_there_are_no_children_and_no_items : given.an_empty_schema_route
{
Because of = () => Configure();

It should_set_name_on_descriptor = () => object_type_descriptor.Verify(_ => _.Name(type_name), Once());
It should_add_a_default_field = () => object_type_descriptor.Verify(_ => _.Field("Default"), Once());
}
}
1 change: 1 addition & 0 deletions Source/DotNET/Backend/Backend.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageReference Include="dolittle.sdk" Version="$(DolittleSdkVersion)" />
<PackageReference Include="FluentValidation" Version="9.5.3" />
<PackageReference Include="HotChocolate.AspNetCore" Version="11.0.9" />
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="11.0.9" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="5.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="MongoDB.Driver" Version="2.10.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public static void AddGraphQL(this IServiceCollection services, IContainer conta

var graphQLBuilder = services
.AddGraphQLServer()
.AddAuthorization()
.UseFluentValidation()
.AddType(new UuidType(UuidFormat));
types.FindMultiple<ScalarType>().Where(_ => !_.IsGenericType).ForEach(_ => graphQLBuilder.AddType(_));
Expand Down
26 changes: 24 additions & 2 deletions Source/DotNET/Backend/GraphQL/SchemaRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using HotChocolate.Types;
using Microsoft.AspNetCore.Authorization;

namespace Dolittle.Vanir.Backend.GraphQL
{
Expand All @@ -20,7 +21,7 @@ public SchemaRoute(string path, string localName, string typeName)

public string Path { get; }
public string LocalName { get; }
public string TypeName { get; }
public string TypeName { get; }

public void AddChild(SchemaRoute child)
{
Expand All @@ -38,7 +39,9 @@ protected override void Configure(IObjectTypeDescriptor descriptor)

foreach (var item in _items)
{
descriptor.Field(item.Method).Name(item.Name);
var fieldDescriptor = descriptor.Field(item.Method).Name(item.Name);

AddAdornedAuthorization(item, fieldDescriptor);
}

foreach (var child in _children)
Expand All @@ -51,5 +54,24 @@ protected override void Configure(IObjectTypeDescriptor descriptor)
descriptor.Field("Default").Resolve(() => "Configure your first item");
}
}

void AddAdornedAuthorization(SchemaRouteItem item, IObjectFieldDescriptor fieldDescriptor)
{
var authorizeAttributes = new List<AuthorizeAttribute>();
authorizeAttributes.AddRange(item.Method.GetCustomAttributes(typeof(AuthorizeAttribute), true) as AuthorizeAttribute[]);
authorizeAttributes.AddRange(item.Method.DeclaringType.GetCustomAttributes(typeof(AuthorizeAttribute), true) as AuthorizeAttribute[]);

foreach (var authorizeAttribute in authorizeAttributes)
{
if (string.IsNullOrEmpty(authorizeAttribute.Policy))
{
fieldDescriptor.Authorize();
}
else
{
fieldDescriptor.Authorize(authorizeAttribute.Policy);
}
}
}
}
}

0 comments on commit c34c0ad

Please sign in to comment.