authorization

package
v1.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2023 License: BSD-2-Clause Imports: 4 Imported by: 0

README

@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

  1. Add directive definition to your shema:

        directive @hasRole(role: Role!) on FIELD_DEFINITION
    
  2. Add directive to the protected fields in the schema:

        type Query {
            # other field resolvers here
            privateGreet(name: String!): String! @hasRole(role: ADMIN)
        }
    
  3. 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
    }
    
  4. 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
    }
    
  5. 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...)
    
  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:

    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))
        })
    }
    
  7. Wrap the GraphQL handler with the auth middleware:

        http.Handle("/query", auth(&relay.Handler{Schema: schema}))
    
  8. In order to access the private resolver add a role header like below:

accessing a private resolver using role header

Documentation

Overview

Package authorization contains a simple GraphQL schema using directives.

Index

Constants

View Source
const Schema = `` /* 234-byte string literal not displayed */

Variables

This section is empty.

Functions

This section is empty.

Types

type HasRoleDirective

type HasRoleDirective struct {
	Role string
}

func (*HasRoleDirective) ImplementsDirective

func (h *HasRoleDirective) ImplementsDirective() string

func (*HasRoleDirective) Validate

func (h *HasRoleDirective) Validate(ctx context.Context, _ interface{}) error

type Resolver

type Resolver struct{}

func (*Resolver) PrivateGreet

func (r *Resolver) PrivateGreet(ctx context.Context, args struct{ Name string }) string

func (*Resolver) PublicGreet

func (r *Resolver) PublicGreet(ctx context.Context, args struct{ Name string }) string

Directories

Path Synopsis
package user contains a naive implementation of an user with roles.
package user contains a naive implementation of an user with roles.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL