gqlgen icon indicating copy to clipboard operation
gqlgen copied to clipboard

Directives not applied to entity resolvers in Federation mode

Open ruslanjo opened this issue 1 month ago • 3 comments

Hi, gqlgen team!

We came across an issue with federation generation. Gqlgen does not generate directives on entity resolvers, which creates a security gap when using Apollo Federation.

Problem Description

When using Apollo Federation, directives (like @guard) are correctly applied to query resolvers but are completely bypassed for entity resolvers (_entities query). This means that authorization checks work for direct queries but being skipped when Apollo Router fetches the same entity through federation's entity resolution mechanism.

Environment

gqlgen version: v0.17.63 Federation version: v2 Go version: 1.24

# schema
directive @guard(name: String, parent: String, skip: String) on OBJECT

type Person @key(fields: "id") @guard(name: "PersonGuard") {
    id: String!
    email: String!
    phone: String!
}

extend type Query {
    getPerson(id: String!): Person
}

gqlgen configuration

# gqlgen.yml
federation:
  version: 2

generated code for query resolver

// generated/schema.generated.go
func (ec *executionContext) _Query_getPerson(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
    resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
        directive0 := func(rctx context.Context) (any, error) {
            return ec.resolvers.Query().GetPerson(rctx, personID)
        }

        directive1 := func(ctx context.Context) (any, error) {
            name, err := ec.unmarshalOString2ᚖstring(ctx, "PersonGuard")
            if err != nil {
                return nil, err
            }
            // ✅ Guard IS applied here
            return ec.directives.Guard(ctx, nil, directive0, name, nil, nil)
        }

        return directive1(rctx)
    })
    // ...
}

generated code for entity resolver (directivies aren't being applied)

// generated/federation.go
func (ec *executionContext) resolveEntity(
    ctx context.Context,
    typeName string,
    rep EntityRepresentation,
) (e fedruntime.Entity, err error) {
    switch typeName {
    case "Person":
        id, err := ec.unmarshalNString2string(ctx, rep["id"])
        if err != nil {
            return nil, err
        }
        // ❌ Guard is NOT applied - direct resolver call
        entity, err := ec.resolvers.Entity().FindPersonByID(ctx, id)
        if err != nil {
            return nil, err
        }
        return entity, nil
    }
}

ruslanjo avatar Nov 12 '25 12:11 ruslanjo

@ruslanjo Thanks for the reproducible example! This would make a great (currently failing) test case for a PR, and I would really appreciate it if you could contribute that, as well as any further improvements!

StevenACoffman avatar Nov 12 '25 13:11 StevenACoffman

@StevenACoffman Thanks for the answer! Just to confirm — would you like me to open a PR that only adds the failing test case for now, or are you expecting a fix as well?

ruslanjo avatar Nov 13 '25 10:11 ruslanjo

Either! I would appreciate the failing test as a PR, but if you can contribute a fix, that would be even better.

StevenACoffman avatar Nov 15 '25 03:11 StevenACoffman