Skip to content

Commit 0a3b9e6

Browse files
committed
Implemented per field authorization using ASP.NET authorization policies
1 parent 41680d1 commit 0a3b9e6

File tree

6 files changed

+189
-26
lines changed

6 files changed

+189
-26
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
namespace FSharp.Data.GraphQL.Samples.StarWarsApi.Middleware
2+
3+
open System
4+
open System.Threading.Tasks
5+
open Microsoft.FSharp.Quotations
6+
open Microsoft.FSharp.Quotations.Patterns
7+
open Microsoft.FSharp.Linq.RuntimeHelpers
8+
open Microsoft.AspNetCore.Authorization
9+
open Microsoft.AspNetCore.Http
10+
open Microsoft.Extensions.DependencyInjection
11+
12+
open FSharp.Data.GraphQL
13+
open FSharp.Data.GraphQL.Types
14+
open FSharp.Data.GraphQL.Samples.StarWarsApi
15+
16+
type FieldPolicyMiddleware<'Val, 'Res> = ResolveFieldContext -> 'Val -> (ResolveFieldContext -> 'Val -> Async<'Res>) -> Async<'Res>
17+
18+
type internal CustomPolicyFieldDefinition<'Val, 'Res> (source : FieldDef<'Val, 'Res>, middleware : FieldPolicyMiddleware<'Val, 'Res>) =
19+
20+
interface FieldDef<'Val, 'Res> with
21+
22+
member _.Name = source.Name
23+
member _.Description = source.Description
24+
member _.DeprecationReason = source.DeprecationReason
25+
member _.TypeDef = source.TypeDef
26+
member _.Args = source.Args
27+
member _.Metadata = source.Metadata
28+
member _.Resolve =
29+
30+
let changeAsyncResolver expr =
31+
let expr =
32+
match expr with
33+
| WithValue (_, _, e) -> e
34+
| _ -> failwith "Unexpected resolver expression."
35+
let resolver =
36+
<@ fun ctx input -> middleware ctx input (%%expr : ResolveFieldContext -> 'Val -> Async<'Res>) @>
37+
let compiledResolver = LeafExpressionConverter.EvaluateQuotation resolver
38+
Expr.WithValue (compiledResolver, resolver.Type, resolver)
39+
40+
let changeSyncResolver expr =
41+
let expr =
42+
match expr with
43+
| WithValue (_, _, e) -> e
44+
| _ -> failwith "Unexpected resolver expression."
45+
let resolver =
46+
<@
47+
fun ctx input ->
48+
middleware ctx input (fun ctx input ->
49+
((%%expr : ResolveFieldContext -> 'Val -> 'Res) ctx input)
50+
|> async.Return)
51+
@>
52+
let compiledResolver = LeafExpressionConverter.EvaluateQuotation resolver
53+
Expr.WithValue (compiledResolver, resolver.Type, resolver)
54+
55+
match source.Resolve with
56+
| Sync (input, output, expr) -> Async (input, output, changeSyncResolver expr)
57+
| Async (input, output, expr) -> Async (input, output, changeAsyncResolver expr)
58+
| Undefined -> failwith "Field has no resolve function."
59+
| x -> failwith <| sprintf "Resolver '%A' is not supported." x
60+
61+
interface IEquatable<FieldDef> with
62+
member _.Equals (other) = source.Equals (other)
63+
64+
override _.Equals y = source.Equals y
65+
override _.GetHashCode () = source.GetHashCode ()
66+
override _.ToString () = source.ToString ()
67+
68+
[<AutoOpen>]
69+
module TypeSystemExtensions =
70+
71+
let handlePolicies (policies : string array) (ctx : ResolveFieldContext) value = async {
72+
73+
let root : Root = downcast ctx.Context.RootValue
74+
let serviceProvider = root.ServiceProvider
75+
let authorizationService = serviceProvider.GetRequiredService<IAuthorizationService> ()
76+
let principal = serviceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext.User
77+
78+
let! authorizationResults =
79+
policies
80+
|> Seq.map (fun p -> authorizationService.AuthorizeAsync (principal, value, p))
81+
|> Seq.toArray
82+
|> Task.WhenAll
83+
|> Async.AwaitTask
84+
85+
let requirements =
86+
authorizationResults
87+
|> Seq.where (fun r -> not r.Succeeded)
88+
|> Seq.collect (fun r -> r.Failure.FailedRequirements)
89+
90+
if Seq.isEmpty requirements
91+
then return Ok ()
92+
else return Error "Forbidden"
93+
}
94+
95+
[<Literal>]
96+
let AuthorizationPolicy = "AuthorizationPolicy"
97+
98+
type FieldDef<'Val, 'Res> with
99+
100+
member this.WithPolicyMiddleware<'Val, 'Res> (middleware : FieldPolicyMiddleware<'Val, 'Res>) : FieldDef<'Val, 'Res> =
101+
upcast CustomPolicyFieldDefinition (this, middleware)
102+
103+
member field.WithAuthorizationPolicies<'Val, 'Res> ([<ParamArray>] policies : string array) : FieldDef<'Val, 'Res> =
104+
105+
let middleware ctx value (resolver : ResolveFieldContext -> 'Val -> Async<'Res>) : Async<'Res> = async {
106+
let! result = handlePolicies policies ctx value
107+
match result with
108+
| Ok _ -> return! resolver ctx value
109+
| Error error -> return raise (GQLMessageException error)
110+
}
111+
112+
field.WithPolicyMiddleware<'Val, 'Res> middleware

samples/star-wars-api/Policies.fs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace FSharp.Data.GraphQL.Samples.StarWarsApi.Authorization
2+
3+
open FSharp.Core
4+
open Microsoft.AspNetCore.Authorization
5+
6+
module Policies =
7+
8+
let [<Literal>] CanSetMoon = "CanSetMoon"
9+
10+
type IsCharacterRequierment (character : string list) =
11+
member val Characters = character
12+
interface IAuthorizationRequirement
13+
14+
type IsCharacterHandler () =
15+
16+
inherit AuthorizationHandler<IsCharacterRequierment> () // Inject services from DI
17+
18+
override _.HandleRequirementAsync (context, requirement) =
19+
Async.StartAsTask(async {
20+
let allowedCharacters = requirement.Characters
21+
if context.User.Claims
22+
|> Seq.where (fun c -> c.Type = "character")
23+
|> Seq.exists (fun c -> allowedCharacters |> List.contains c.Value)
24+
then context.Succeed requirement
25+
else () // Go to the next handler if registered
26+
}) :> _

samples/star-wars-api/Root.fs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace FSharp.Data.GraphQL.Samples.StarWarsApi
2+
3+
open System
4+
open Microsoft.AspNetCore.Http
5+
open Microsoft.Extensions.DependencyInjection
6+
7+
type Root(ctx : HttpContext) =
8+
9+
member _.RequestId = ctx.TraceIdentifier
10+
member _.RequestAborted: System.Threading.CancellationToken = ctx.RequestAborted
11+
member _.ServiceProvider: IServiceProvider = ctx.RequestServices
12+
member root.GetRequiredService<'t>() = root.ServiceProvider.GetRequiredService<'t>()

samples/star-wars-api/Schema.fs

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,10 @@
11
namespace FSharp.Data.GraphQL.Samples.StarWarsApi
22

3-
open System
4-
open Microsoft.AspNetCore.Http
5-
open Microsoft.Extensions.DependencyInjection
6-
7-
type Root(ctx : HttpContext) =
8-
9-
member _.RequestId = ctx.TraceIdentifier
10-
member _.RequestAborted: System.Threading.CancellationToken = ctx.RequestAborted
11-
member _.ServiceProvider: IServiceProvider = ctx.RequestServices
12-
member root.GetRequiredService<'t>() = root.ServiceProvider.GetRequiredService<'t>()
13-
143
open FSharp.Data.GraphQL
154
open FSharp.Data.GraphQL.Types
165
open FSharp.Data.GraphQL.Server.Relay
176
open FSharp.Data.GraphQL.Server.Middleware
7+
open FsToolkit.ErrorHandling
188

199
#nowarn "40"
2010

@@ -277,24 +267,36 @@ module Schema =
277267

278268
let schemaConfig = SchemaConfig.Default
279269

270+
open FSharp.Data.GraphQL.Samples.StarWarsApi.Middleware
271+
open FSharp.Data.GraphQL.Samples.StarWarsApi.Authorization
272+
280273
let Mutation =
281274
Define.Object<Root> (
282275
name = "Mutation",
283-
fields =
284-
[ Define.Field(
285-
"setMoon",
286-
Nullable PlanetType,
287-
"Defines if a planet is actually a moon or not.",
288-
[ Define.Input ("id", StringType); Define.Input ("isMoon", BooleanType) ],
289-
fun ctx _ ->
290-
getPlanet (ctx.Arg ("id"))
291-
|> Option.map (fun x ->
292-
x.SetMoon (Some (ctx.Arg ("isMoon"))) |> ignore
293-
schemaConfig.SubscriptionProvider.Publish<Planet> "watchMoon" x
294-
schemaConfig.LiveFieldSubscriptionProvider.Publish<Planet> "Planet" "isMoon" x
295-
x)
296-
)
297-
]
276+
fields = [
277+
let setMoon (ctx : ResolveFieldContext) (_ : Root) = option {
278+
let! planet = getPlanet (ctx.Arg ("id"))
279+
ignore (planet.SetMoon (Some (ctx.Arg ("isMoon"))))
280+
schemaConfig.SubscriptionProvider.Publish<Planet> "watchMoon" planet
281+
schemaConfig.LiveFieldSubscriptionProvider.Publish<Planet> "Planet" "isMoon" planet
282+
return planet
283+
}
284+
Define.Field(
285+
"setMoon",
286+
Nullable PlanetType,
287+
"Defines if a planet is actually a moon or not.",
288+
[ Define.Input ("id", StringType); Define.Input ("isMoon", BooleanType) ],
289+
setMoon
290+
// Using complex lambda crashes
291+
//(fun ctx _ -> option {
292+
// let! planet = getPlanet (ctx.Arg ("id"))
293+
// ignore (planet.SetMoon (Some (ctx.Arg ("isMoon"))))
294+
// schemaConfig.SubscriptionProvider.Publish<Planet> "watchMoon" planet
295+
// schemaConfig.LiveFieldSubscriptionProvider.Publish<Planet> "Planet" "isMoon" planet
296+
// return planet
297+
//})
298+
).WithAuthorizationPolicies(Policies.CanSetMoon)
299+
]
298300
)
299301

300302
let schema : ISchema<Root> = upcast Schema (Query, Mutation, Subscription, schemaConfig)

samples/star-wars-api/Startup.fs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace FSharp.Data.GraphQL.Samples.StarWarsApi
22

33
open System
44
open System.Threading.Tasks
5+
open Microsoft.AspNetCore.Authorization
56
open Microsoft.AspNetCore.Builder
67
open Microsoft.AspNetCore.Http
78
open Microsoft.Extensions.Configuration
@@ -11,6 +12,7 @@ open Microsoft.Extensions.Hosting
1112
open Oxpecker
1213
open FSharp.Data.GraphQL.Server.AspNetCore
1314
open FSharp.Data.GraphQL.Server.AspNetCore.Oxpecker
15+
open FSharp.Data.GraphQL.Samples.StarWarsApi.Authorization
1416

1517
type Startup private () =
1618

@@ -28,6 +30,12 @@ type Startup private () =
2830

2931
member _.ConfigureServices (services : IServiceCollection) : unit =
3032
services
33+
.AddAuthorization(fun options ->
34+
options.AddPolicy (
35+
Policies.CanSetMoon,
36+
(fun policy -> policy.Requirements.Add (IsCharacterRequierment (List.singleton "droid"))))
37+
)
38+
.AddScoped<IAuthorizationHandler, IsCharacterHandler>()
3139
.AddOxpecker()
3240
.AddGraphQL<Root> (Schema.executor, rootFactory, configure = configure)
3341
|> ignore

samples/star-wars-api/star-wars-api.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
<ItemGroup>
1919
<None Include="ApplicationInsights.config" />
20+
<Compile Include="Root.fs" />
21+
<Compile Include="Policies.fs" />
22+
<Compile Include="AuthorizationMiddleware.fs" />
2023
<Compile Include="Schema.fs" />
2124
<None Include="MultipartRequest.fs" />
2225
<Compile Include="Startup.fs" />

0 commit comments

Comments
 (0)