graph-gophers / graphql-go

GraphQL server with a focus on ease of use
BSD 2-Clause "Simplified" License
4.65k stars 492 forks source link

Directives import in example code seems wrong #641

Closed zaydek closed 6 months ago

zaydek commented 6 months ago
func main() {
    opts := []graphql.SchemaOpt{
        graphql.Directives(&authorization.HasRoleDirective{}),
        // other options go here
    }

https://github.com/graph-gophers/graphql-go/blob/master/example/directives/authorization/server/server.go

Hi Pavel, I could be mistaken but isn't this supposed to be directives.Directives from "github.com/graph-gophers/graphql-go/directives"?

Also, am I right in assuming this is a v1.6 feature release and not supported yet (as of v1.5.0)?

zaydek commented 6 months ago

Oops nevermind ignore me. I think I just got confused with go get github.com/graph-gophers/graphql-go@master.

pavelnikolov commented 6 months ago

Correct. Actually, this is the feature that is delaying the v1.6 release. I'm still not happy with the implementation. I'm considering a release soon either without the directives support or with a comment that directives are an alpha feature and the API is almost certainly subject to changes.

zaydek commented 6 months ago

I see. I was able to get something basic working based on your provided example:

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"

    "github.com/graph-gophers/graphql-go"
    "github.com/graph-gophers/graphql-go/relay"
    "github.com/sirupsen/logrus"
)

type contextKey string

const ContextAccessAllowedKey contextKey = "context-key"

// setAccessAllowed sets the access allowed value in the context.
func setAccessAllowed(ctx context.Context) context.Context {
    return context.WithValue(ctx, ContextAccessAllowedKey, true)
}

// isAccessAllowed returns the access allowed value from the context.
func isAccessAllowed(ctx context.Context) bool {
    accessAllowed, ok := ctx.Value(ContextAccessAllowedKey).(bool)
    return ok && accessAllowed
}

// Define the schema
const Schema = `
    # directive @hasRole(role: Role!) on FIELD_DEFINITION
    directive @protected on FIELD_DEFINITION

    # enum Role {
    #   ADMIN
    #   USER
    # }

    schema {
        query: Query
    }

    type Query {
        publicGreet(name: String!): String!
        privateGreet(name: String!): String! @protected

        # privateGreet(name: String!): String! @hasRole(role: ADMIN)
    }
`

// Define the protected directive
type ProtectedDirective struct{}

func (p *ProtectedDirective) ImplementsDirective() string { return "protected" }
func (p *ProtectedDirective) Validate(ctx context.Context, _ interface{}) error {
    if isAccessAllowed(ctx) {
        return nil
    } else {
        return errors.New("access denied")
    }
}

// Define the GraphiQL HTML
var graphiql = []byte(`
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>GraphiQL</title>
    <style>
      body {
        height: 100%;
        margin: 0;
        width: 100%;
        overflow: hidden;
      }
      #graphiql {
        height: 100vh;
      }
    </style>
    <script src="https://unpkg.com/react@17/umd/react.development.js" integrity="sha512-Vf2xGDzpqUOEIKO+X2rgTLWPY+65++WPwCHkX2nFMu9IcstumPsf/uKKRd5prX3wOu8Q0GBylRpsDB26R6ExOg==" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" integrity="sha512-Wr9OKCTtq1anK0hq5bY3X/AvDI5EflDSAh0mE9gma+4hl+kXdTJPKZ3TwLMBcrgUeoY0s3dq9JjhCQc7vddtFg==" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://unpkg.com/graphiql@2.3.0/graphiql.min.css" />
  </head>
  <body>
    <div id="graphiql">Loading...</div>
    <script src="https://unpkg.com/graphiql@2.3.0/graphiql.min.js" type="application/javascript"></script>
    <script>
      ReactDOM.render(
        React.createElement(GraphiQL, {
          fetcher: GraphiQL.createFetcher({url: '/query'}),
          defaultEditorToolsVisibility: true,
        }),
        document.getElementById('graphiql'),
      );
    </script>
  </body>
</html>
`)

func main() {
    // Define the schema
    schemaOptions := []graphql.SchemaOpt{graphql.Directives(&ProtectedDirective{})}
    schema := graphql.MustParseSchema(Schema, &Resolver{}, schemaOptions...)

    // Setup the handlers for GraphiQL and GraphQL
    http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(graphiql) }))
    http.Handle("/query", withProtected(&relay.Handler{Schema: schema}))

    logrus.Info("Listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

const HeaderKey = "debug_is_protected"

// TODO: Clean this up. The roles implementation is too confusing right now. In
// theory this can be as simple as reading a boolean context value.
func withProtected(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()

        // This is a hack to allow the protected directive to work. In production,
        // this would more likely than not check the rights of a session and inject
        // the relevant values into the context. This allows the directive to ensure
        // the values are present before continuing. Then any routes that need
        // access to said values can read the context.
        value := r.Header.Get(HeaderKey)
        if value == "true" {
            fmt.Println("AA")
            ctx = setAccessAllowed(r.Context())
        } else {
            fmt.Println("BB")
        }
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

type Resolver struct{}

func (r *Resolver) PublicGreet(ctx context.Context, args struct{ Name string }) string {
    return fmt.Sprintf("Hello from the public resolver, %s!", args.Name)
}

func (r *Resolver) PrivateGreet(ctx context.Context, args struct{ Name string }) string {
    return fmt.Sprintf("Hi from the protected resolver, %s!", args.Name)
}

I just want a simple protected directive which this provides, then in my app I can go ahead and refine it so I inspect the X-Session-ID on headers, "stat" for user privileges, then inject that into the context. So far I don't have a need for the role-based implementation but I can understand that is probably better for some things.

a comment that directives are an alpha feature and the API is almost certainly subject to changes.

The public-facing API makes a lot of sense to me. It feels isomorphic to setting up resolvers. Totally fine if you move things around, I'll probably continue to experiment with whatever I have access to, as of now: github.com/graph-gophers/graphql-go v1.5.1-0.20240411081201-c3bd44b3b227.

If you do want to release it as an alpha you could always prefix the API so that it's subject to change -- like graphql.Directives -> graphql.UNSTABLE_Directives or so on. Personally I'd appreciate having something to experiment with, but it doesn't have to be 1.6 so long as I can continue to use go get.

It's a really cool feature and makes a lot more sense than a DIY solution based on inspecting the query and using a switch or something. At least those are my initial impressions.