@hasRole directive
Overview
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)
.
Getting started
To run this server
go run ./example/directives/authorization/server/server.go
Navigate to https://localhost:8080 in your browser to interact with the GraphiQL UI.
Testing with curl
Access public resolver:
$ curl 'http://localhost:8080/query' \
-H 'Accept: application/json' \
--data-raw '{"query":"# mutation {\nquery {\n publicGreet(name: \"John\")\n}","variables":null}'
{"data":{"publicGreet":"Hello from the public resolver, John!"}}
Try accessing protected resolver without required role:
$ curl 'http://localhost:8080/query' \
-H 'Accept: application/json' \
--data-raw '{"query":"# mutation {\nquery {\n privateGreet(name: \"John\")\n}","variables":null}'
{"errors":[{"message":"access denied, \"admin\" role required","path":["privateGreet"]}],"data":null}
Try accessing protected resolver again with appropriate role:
$ curl 'http://localhost:8080/query' \
-H 'Accept: application/json' \
-H 'role: admin' \
--data-raw '{"query":"# mutation {\nquery {\n privateGreet(name: \"John\")\n}","variables":null}'
{"data":{"privateGreet":"Hi from the protected resolver, John!"}}
Implementation details
-
Add directive definition to your shema:
directive @hasRole(role: Role!) on FIELD_DEFINITION
-
Add directive to the protected fields in the schema:
type Query {
# other field resolvers here
privateGreet(name: String!): String! @hasRole(role: ADMIN)
}
-
Define a user Go type which can be assigned to different roles where each role is a string:
type User struct {
ID string
Roles map[string]struct{}
}
func (u *User) AddRole(r string) {
if u.Roles == nil {
u.Roles = map[string]struct{}{}
}
u.Roles[r] = struct{}{}
}
func (u *User) HasRole(r string) bool {
_, ok := u.Roles[r]
return ok
}
-
Define a Go type which implements the directives.Directive
interface:
type HasRoleDirective struct{
Role string
}
func (h *HasRoleDirective) ImplementsDirective() string {
return "hasRole"
}
func (h *HasRoleDirective) Validate(ctx context.Context, _ interface{}) error {
u, ok := user.FromContext(ctx)
if !ok {
return fmt.Errorf("user not provided in context")
}
role := strings.ToLower(h.Role)
if !u.HasRole(role) {
return fmt.Errorf("access denied, %q role required", role)
}
return nil
}
-
Pay attention to the schmema options. Directive visitors are added as schema option:
opts := []graphql.SchemaOpt{
graphql.Directives(
&authorization.HasRoleDirective{},
// additional directives
),
// other options go here
}
schema := graphql.MustParseSchema(authorization.Schema, &authorization.Resolver{}, opts...)
-
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:
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u := &user.User{}
role := r.Header.Get("role")
if role != "" {
u.AddRole(role)
}
ctx := user.AddToContext(context.Background(), u)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
-
Wrap the GraphQL handler with the auth middleware:
http.Handle("/query", auth(&relay.Handler{Schema: schema}))
-
In order to access the private resolver add a role header like below:
