Skip to content

Commit f42e3d7

Browse files
authored
Merge pull request #488 from fsprojects/authorization_middleware
Authorization middleware sample
2 parents 41680d1 + 6615536 commit f42e3d7

File tree

9 files changed

+235
-50
lines changed

9 files changed

+235
-50
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
try
53+
let compiledResolver = LeafExpressionConverter.EvaluateQuotation resolver
54+
Expr.WithValue (compiledResolver, resolver.Type, resolver)
55+
with :? NotSupportedException as ex ->
56+
let message =
57+
$"F# compiler cannot convert '{source.Name}' field resolver expression to LINQ, use function instead"
58+
raise (NotSupportedException (message, ex))
59+
60+
match source.Resolve with
61+
| Sync (input, output, expr) -> Async (input, output, changeSyncResolver expr)
62+
| Async (input, output, expr) -> Async (input, output, changeAsyncResolver expr)
63+
| Undefined -> failwith "Field has no resolve function."
64+
| x -> failwith <| sprintf "Resolver '%A' is not supported." x
65+
66+
interface IEquatable<FieldDef> with
67+
member _.Equals (other) = source.Equals (other)
68+
69+
override _.Equals y = source.Equals y
70+
override _.GetHashCode () = source.GetHashCode ()
71+
override _.ToString () = source.ToString ()
72+
73+
[<AutoOpen>]
74+
module TypeSystemExtensions =
75+
76+
let handlePolicies (policies : string array) (ctx : ResolveFieldContext) value = async {
77+
78+
let root : Root = downcast ctx.Context.RootValue
79+
let serviceProvider = root.ServiceProvider
80+
let authorizationService = serviceProvider.GetRequiredService<IAuthorizationService> ()
81+
let principal = serviceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext.User
82+
83+
let! authorizationResults =
84+
policies
85+
|> Seq.map (fun p -> authorizationService.AuthorizeAsync (principal, value, p))
86+
|> Seq.toArray
87+
|> Task.WhenAll
88+
|> Async.AwaitTask
89+
90+
let failedRequirements =
91+
authorizationResults
92+
|> Seq.where (fun r -> not r.Succeeded)
93+
|> Seq.collect (fun r -> r.Failure.FailedRequirements)
94+
95+
if Seq.isEmpty failedRequirements then
96+
return Ok ()
97+
else
98+
return Error "Forbidden"
99+
}
100+
101+
[<Literal>]
102+
let AuthorizationPolicy = "AuthorizationPolicy"
103+
104+
type FieldDef<'Val, 'Res> with
105+
106+
member field.WithPolicyMiddleware<'Val, 'Res> (middleware : FieldPolicyMiddleware<'Val, 'Res>) : FieldDef<'Val, 'Res> =
107+
upcast CustomPolicyFieldDefinition (field, middleware)
108+
109+
member field.WithAuthorizationPolicies<'Val, 'Res> ([<ParamArray>] policies : string array) : FieldDef<'Val, 'Res> =
110+
111+
let middleware ctx value (resolver : ResolveFieldContext -> 'Val -> Async<'Res>) : Async<'Res> = async {
112+
let! result = handlePolicies policies ctx value
113+
match result with
114+
| Ok _ -> return! resolver ctx value
115+
| Error error -> return raise (GQLMessageException error)
116+
}
117+
118+
field.WithPolicyMiddleware<'Val, 'Res> middleware

samples/star-wars-api/Policies.fs

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

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: 31 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,39 @@ 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+
ctx.Schema.SubscriptionProvider.Publish<Planet> "watchMoon" planet
281+
ctx.Schema.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+
// let planet = planet.SetMoon (Some (ctx.Arg ("isMoon")))
294+
// ctx.Schema.SubscriptionProvider.Publish<Planet> "watchMoon" planet
295+
// ctx.Schema.LiveFieldSubscriptionProvider.Publish<Planet> "Planet" "isMoon" planet
296+
// return planet
297+
//})
298+
// For demo purposes of authorization
299+
//).WithAuthorizationPolicies(Policies.CanSetMoon)
300+
// For build verification purposes
301+
).WithAuthorizationPolicies(Policies.Dummy)
302+
]
298303
)
299304

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

samples/star-wars-api/Startup.fs

Lines changed: 10 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,14 @@ type Startup private () =
2830

2931
member _.ConfigureServices (services : IServiceCollection) : unit =
3032
services
33+
.AddAuthorization(fun options ->
34+
options.AddPolicy (Policies.Dummy, fun policy -> policy.Requirements.Add (DummyRequirement ()))
35+
options.AddPolicy(
36+
Policies.CanSetMoon,
37+
(fun policy -> policy.Requirements.Add (IsCharacterRequirement (Set.singleton "droid"))))
38+
)
39+
.AddScoped<IAuthorizationHandler, DummyHandler>()
40+
.AddScoped<IAuthorizationHandler, IsCharacterHandler>()
3141
.AddOxpecker()
3242
.AddGraphQL<Root> (Schema.executor, rootFactory, configure = configure)
3343
|> 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" />

src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ type GraphQLRequestHandler<'Root> (
232232
let root = options.CurrentValue.RootFactory ctx
233233

234234
let! result =
235-
Async.StartAsTask(
235+
Async.StartImmediateAsTask(
236236
executor.AsyncExecute(content.Ast, root, ?variables = variables, ?operationName = operationName),
237237
cancellationToken = ctx.RequestAborted
238238
)

src/FSharp.Data.GraphQL.Shared/AsyncVal.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ module AsyncVal =
6262
let toTask (x : AsyncVal<'T>) =
6363
match x with
6464
| Value v -> Task.FromResult (v)
65-
| Async a -> Async.StartAsTask (a)
65+
| Async a -> Async.StartImmediateAsTask (a)
6666
| Failure f -> Task.FromException<'T> (f)
6767

6868
/// Returns an empty AsyncVal with immediatelly executed value.

0 commit comments

Comments
 (0)