|
| 1 | +# @hasRole directive |
| 2 | + |
| 3 | +## Overview |
| 4 | +A simple example of naive authorization directive which returns an error if the user in the context doesn't have the required role. Make sure that in production applications you use thread-safe maps for roles as an instance of the user struct might be accessed from multiple goroutines. In this naive example we use a simeple map which is not thread-safe. The required role to access a resolver is passed as an argument to the directive, for example, `@hasRole(role: ADMIN)`. |
| 5 | + |
| 6 | +## Getting started |
| 7 | +To run this server |
| 8 | + |
| 9 | +`go run ./example/directives/authorization/server/server.go` |
| 10 | + |
| 11 | +Navigate to https://localhost:8080 in your browser to interact with the Graph<i>i</i>QL UI. |
| 12 | + |
| 13 | +## Testing with curl |
| 14 | +Access public resolver: |
| 15 | +``` |
| 16 | +$ curl 'http://localhost:8080/query' \ |
| 17 | + -H 'Accept: application/json' \ |
| 18 | + --data-raw '{"query":"# mutation {\nquery {\n publicGreet(name: \"John\")\n}","variables":null}' |
| 19 | +
|
| 20 | +{"data":{"publicGreet":"Hello from the public resolver, John!"}} |
| 21 | +``` |
| 22 | +Try accessing protected resolver without required role: |
| 23 | +``` |
| 24 | +$ curl 'http://localhost:8080/query' \ |
| 25 | + -H 'Accept: application/json' \ |
| 26 | + --data-raw '{"query":"# mutation {\nquery {\n privateGreet(name: \"John\")\n}","variables":null}' |
| 27 | +{"errors":[{"message":"access denied, \"admin\" role required","path":["privateGreet"]}],"data":null} |
| 28 | +``` |
| 29 | +Try accessing protected resolver again with appropriate role: |
| 30 | +``` |
| 31 | +$ curl 'http://localhost:8080/query' \ |
| 32 | + -H 'Accept: application/json' \ |
| 33 | + -H 'role: admin' \ |
| 34 | + --data-raw '{"query":"# mutation {\nquery {\n privateGreet(name: \"John\")\n}","variables":null}' |
| 35 | +{"data":{"privateGreet":"Hi from the protected resolver, John!"}} |
| 36 | +``` |
| 37 | + |
| 38 | +## Implementation details |
| 39 | + |
| 40 | +1. Add directive definition to your shema: |
| 41 | + ```graphql |
| 42 | + directive @hasRole(role: Role!) on FIELD_DEFINITION |
| 43 | + ``` |
| 44 | + |
| 45 | +2. Add directive to the protected fields in the schema: |
| 46 | + ```graphql |
| 47 | + type Query { |
| 48 | + # other field resolvers here |
| 49 | + privateGreet(name: String!): String! @hasRole(role: ADMIN) |
| 50 | + } |
| 51 | + ``` |
| 52 | + |
| 53 | +3. Define a user Go type which can have a slice of roles where each role is a string: |
| 54 | + ```go |
| 55 | + type User struct { |
| 56 | + ID string |
| 57 | + Roles map[string]struct{} |
| 58 | + } |
| 59 | + |
| 60 | + func (u *User) AddRole(r string) { |
| 61 | + if u.Roles == nil { |
| 62 | + u.Roles = map[string]struct{}{} |
| 63 | + } |
| 64 | + u.Roles[r] = struct{}{} |
| 65 | + } |
| 66 | + |
| 67 | + func (u *User) HasRole(r string) bool { |
| 68 | + _, ok := u.Roles[r] |
| 69 | + return ok |
| 70 | + } |
| 71 | + ``` |
| 72 | + |
| 73 | +4. Define a Go type which implements the DirevtiveVisitor interface: |
| 74 | + ```go |
| 75 | + type HasRoleDirective struct{} |
| 76 | + |
| 77 | + func (h *HasRoleDirective) Before(ctx context.Context, directive *types.Directive, input interface{}) (bool, error) { |
| 78 | + u, ok := user.FromContext(ctx) |
| 79 | + if !ok { |
| 80 | + return true, fmt.Errorf("user not provided in cotext") |
| 81 | + } |
| 82 | + role := strings.ToLower((directive.Arguments.MustGet("role").String()) |
| 83 | + if !u.HasRole(role) { |
| 84 | + return true, fmt.Errorf("access denied, %q role required", role) |
| 85 | + } |
| 86 | + return false, nil |
| 87 | + } |
| 88 | + |
| 89 | + // After is a no-op and returns the output unchanged. |
| 90 | + func (h *HasRoleDirective) After(ctx context.Context, directive *types.Directive, output interface{}) (interface{}, error) { |
| 91 | + return output, nil |
| 92 | + } |
| 93 | + ``` |
| 94 | + |
| 95 | +5. Pay attention to the schmema options. Directive visitors are added as schema option: |
| 96 | + ```go |
| 97 | + opts := []graphql.SchemaOpt{ |
| 98 | + graphql.DirectiveVisitors(map[string]directives.Visitor{ |
| 99 | + "hasRole": &authorization.HasRoleDirective{}, |
| 100 | + }), |
| 101 | + // other options go here |
| 102 | + } |
| 103 | + schema := graphql.MustParseSchema(authorization.Schema, &authorization.Resolver{}, opts...) |
| 104 | + ``` |
| 105 | + |
| 106 | +6. Add a middleware to the HTTP handler which would read the `role` HTTP header and add that role to the slice of user roles. This naive middleware assumes that there is authentication proxy (e.g. Nginx, Envoy, Contour etc.) in front of this server which would authenticate the user and add their role in a header. In production application it would be fine if the same application handles the authentication and adds the user to the context. This is the middleware in this example: |
| 107 | + ```go |
| 108 | + func auth(next http.Handler) http.Handler { |
| 109 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 110 | + u := &user.User{} |
| 111 | + role := r.Header.Get("role") |
| 112 | + if role != "" { |
| 113 | + u.AddRole(role) |
| 114 | + } |
| 115 | + ctx := user.AddToContext(context.Background(), u) |
| 116 | + next.ServeHTTP(w, r.WithContext(ctx)) |
| 117 | + }) |
| 118 | + } |
| 119 | + ``` |
| 120 | + |
| 121 | +7. Wrap the GraphQL handler with the auth middleware: |
| 122 | + ```go |
| 123 | + http.Handle("/query", auth(&relay.Handler{Schema: schema})) |
| 124 | + ``` |
| 125 | + |
| 126 | +8. In order to access the private resolver add a role header like below: |
| 127 | + |
| 128 | + |
0 commit comments