Skip to content

Commit 6900573

Browse files
authored
fix(graphql): return gin context error uniformly across all resolvers (#638)
* fix(graphql): return gin context error uniformly across all resolvers All 50 GraphQL resolver files previously used `gc, _ := utils.GinContextFromContext(ctx)`, silently discarding the error and relying on MetaFromGin's nil safety. This is fine at runtime but masks programming errors and leaks an inconsistent contract: resolvers that call ApplyToGin(gc, side) would panic on a nil gc, while read-only resolvers would silently produce empty request metadata. Apply the pattern already established in signup.go uniformly: check the error from GinContextFromContext and return it immediately. The caller gets the actual error ("could not retrieve gin.Context") instead of a downstream auth failure or a nil-pointer panic. * feat(graphql): add log, metric, and tests for gin context missing For all 51 GraphQL resolvers the previous error branch was: return nil, err Now it is: g.Log.Debug().Err(err).Msg("failed to get gin context") metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql") return nil, err Changes: - internal/metrics/metrics.go: add SecurityEventGinContextMissing constant - internal/graphql/*.go (51 files): add structured debug log and security metric to the gin context error branch; add metrics import - internal/integration_tests/gin_context_missing_test.go: 9 subtests covering resolvers with/without ApplyToGin and with/without auth, each called with context.Background() (no embedded gin.Context) Not affected: - HTTP handlers receive gin.Context directly as a function parameter - gRPC handlers use transport.MetaFromGRPC which degrades gracefully when no gRPC metadata is present (already tested in internal/grpcsrv/transport/grpc_metadata_test.go TestMetaFromGRPC_NoMetadata) * fix: route GORM logger to stderr and detach goroutines from request ctx ## GORM stdout → stderr (fixes smoke mcp_stdio) GORM's default logger writes to os.Stdout. The MCP server speaks JSON-RPC over stdio, so any stray stdout line (slow-query warnings, AutoMigrate errors) corrupts the stream and produces "unexpected end of JSON input" on the client. PR #632 scoped logger.Discard to clearLegacyColumnUniqueness but the global gorm.Config had no Logger set, leaving AutoMigrate and all other GORM operations on the default stdout logger. Fix: wire a stderr-backed GORM logger into ormConfig so every GORM operation routes to stderr rather than stdout. Slow-query warnings and errors remain visible; stdout stays clean for stdio transports. ## context.WithoutCancel in fire-and-forget goroutines 18 goroutines across 9 service files were capturing the request ctx and using it for EventsProvider.RegisterEvent, StorageProvider.AddSession, etc. If the request context is cancelled (client disconnect, upstream timeout), those goroutines would return early without firing webhooks or persisting sessions. Fix: inject ctx := context.WithoutCancel(ctx) as the first statement in every goroutine that uses the request ctx. This preserves any context values (trace IDs, logger) while detaching from the cancellation signal, so side effects complete independently of the request lifetime. Goroutines that do not use ctx (MemoryStoreProvider calls, EmailProvider calls whose signature takes no context) are left unchanged.
1 parent c37df71 commit 6900573

64 files changed

Lines changed: 516 additions & 52 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

internal/graphql/add_email_template.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/authorizerdev/authorizer/internal/graph/model"
7+
"github.com/authorizerdev/authorizer/internal/metrics"
78
"github.com/authorizerdev/authorizer/internal/service"
89
"github.com/authorizerdev/authorizer/internal/utils"
910
)
@@ -13,7 +14,12 @@ import (
1314
//
1415
// Permissions: authorizer:admin
1516
func (g *graphqlProvider) AddEmailTemplate(ctx context.Context, params *model.AddEmailTemplateRequest) (*model.Response, error) {
16-
gc, _ := utils.GinContextFromContext(ctx)
17+
gc, err := utils.GinContextFromContext(ctx)
18+
if err != nil {
19+
g.Log.Debug().Err(err).Msg("failed to get gin context")
20+
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
21+
return nil, err
22+
}
1723
res, _, err := g.adminService().AddEmailTemplate(ctx, service.MetaFromGin(gc), params)
1824
return res, err
1925
}

internal/graphql/add_webhook.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/authorizerdev/authorizer/internal/graph/model"
7+
"github.com/authorizerdev/authorizer/internal/metrics"
78
"github.com/authorizerdev/authorizer/internal/service"
89
"github.com/authorizerdev/authorizer/internal/utils"
910
)
@@ -13,7 +14,12 @@ import (
1314
//
1415
// Permissions: authorizer:admin
1516
func (g *graphqlProvider) AddWebhook(ctx context.Context, params *model.AddWebhookRequest) (*model.Response, error) {
16-
gc, _ := utils.GinContextFromContext(ctx)
17+
gc, err := utils.GinContextFromContext(ctx)
18+
if err != nil {
19+
g.Log.Debug().Err(err).Msg("failed to get gin context")
20+
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
21+
return nil, err
22+
}
1723
res, _, err := g.adminService().AddWebhook(ctx, service.MetaFromGin(gc), params)
1824
return res, err
1925
}

internal/graphql/admin_login.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/authorizerdev/authorizer/internal/graph/model"
7+
"github.com/authorizerdev/authorizer/internal/metrics"
78
"github.com/authorizerdev/authorizer/internal/service"
89
"github.com/authorizerdev/authorizer/internal/utils"
910
)
@@ -13,7 +14,12 @@ import (
1314
//
1415
// Permissions: none
1516
func (g *graphqlProvider) AdminLogin(ctx context.Context, params *model.AdminLoginRequest) (*model.Response, error) {
16-
gc, _ := utils.GinContextFromContext(ctx)
17+
gc, err := utils.GinContextFromContext(ctx)
18+
if err != nil {
19+
g.Log.Debug().Err(err).Msg("failed to get gin context")
20+
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
21+
return nil, err
22+
}
1723
res, side, err := g.adminService().AdminLogin(ctx, service.MetaFromGin(gc), params)
1824
if err != nil {
1925
return nil, err

internal/graphql/admin_logout.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/authorizerdev/authorizer/internal/graph/model"
7+
"github.com/authorizerdev/authorizer/internal/metrics"
78
"github.com/authorizerdev/authorizer/internal/service"
89
"github.com/authorizerdev/authorizer/internal/utils"
910
)
@@ -13,7 +14,12 @@ import (
1314
//
1415
// Permissions: authorizer:admin
1516
func (g *graphqlProvider) AdminLogout(ctx context.Context) (*model.Response, error) {
16-
gc, _ := utils.GinContextFromContext(ctx)
17+
gc, err := utils.GinContextFromContext(ctx)
18+
if err != nil {
19+
g.Log.Debug().Err(err).Msg("failed to get gin context")
20+
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
21+
return nil, err
22+
}
1723
res, side, err := g.adminService().AdminLogout(ctx, service.MetaFromGin(gc))
1824
if err != nil {
1925
return nil, err

internal/graphql/admin_meta.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/authorizerdev/authorizer/internal/graph/model"
7+
"github.com/authorizerdev/authorizer/internal/metrics"
78
"github.com/authorizerdev/authorizer/internal/service"
89
"github.com/authorizerdev/authorizer/internal/utils"
910
)
@@ -13,7 +14,12 @@ import (
1314
//
1415
// Permissions: authorizer:admin
1516
func (g *graphqlProvider) AdminMeta(ctx context.Context) (*model.AdminMeta, error) {
16-
gc, _ := utils.GinContextFromContext(ctx)
17+
gc, err := utils.GinContextFromContext(ctx)
18+
if err != nil {
19+
g.Log.Debug().Err(err).Msg("failed to get gin context")
20+
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
21+
return nil, err
22+
}
1723
res, _, err := g.adminService().AdminMeta(ctx, service.MetaFromGin(gc))
1824
return res, err
1925
}

internal/graphql/admin_session.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/authorizerdev/authorizer/internal/graph/model"
7+
"github.com/authorizerdev/authorizer/internal/metrics"
78
"github.com/authorizerdev/authorizer/internal/service"
89
"github.com/authorizerdev/authorizer/internal/utils"
910
)
@@ -14,7 +15,12 @@ import (
1415
//
1516
// Permissions: authorizer:admin
1617
func (g *graphqlProvider) AdminSession(ctx context.Context) (*model.Response, error) {
17-
gc, _ := utils.GinContextFromContext(ctx)
18+
gc, err := utils.GinContextFromContext(ctx)
19+
if err != nil {
20+
g.Log.Debug().Err(err).Msg("failed to get gin context")
21+
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
22+
return nil, err
23+
}
1824
res, side, err := g.adminService().AdminSession(ctx, service.MetaFromGin(gc))
1925
if err != nil {
2026
return nil, err

internal/graphql/audit_logs.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/authorizerdev/authorizer/internal/graph/model"
7+
"github.com/authorizerdev/authorizer/internal/metrics"
78
"github.com/authorizerdev/authorizer/internal/service"
89
"github.com/authorizerdev/authorizer/internal/utils"
910
)
@@ -13,7 +14,12 @@ import (
1314
//
1415
// Permissions: authorizer:admin
1516
func (g *graphqlProvider) AuditLogs(ctx context.Context, params *model.ListAuditLogRequest) (*model.AuditLogs, error) {
16-
gc, _ := utils.GinContextFromContext(ctx)
17+
gc, err := utils.GinContextFromContext(ctx)
18+
if err != nil {
19+
g.Log.Debug().Err(err).Msg("failed to get gin context")
20+
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
21+
return nil, err
22+
}
1723
res, _, err := g.adminService().AuditLogs(ctx, service.MetaFromGin(gc), params)
1824
return res, err
1925
}

internal/graphql/check_permissions.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ import (
44
"context"
55

66
"github.com/authorizerdev/authorizer/internal/graph/model"
7+
"github.com/authorizerdev/authorizer/internal/metrics"
78
"github.com/authorizerdev/authorizer/internal/service"
89
"github.com/authorizerdev/authorizer/internal/utils"
910
)
1011

1112
// CheckPermissions delegates to the transport-agnostic service layer, which
1213
// owns the subject trust gate and fail-closed semantics.
1314
func (g *graphqlProvider) CheckPermissions(ctx context.Context, params *model.CheckPermissionsInput) (*model.CheckPermissionsResponse, error) {
14-
gc, _ := utils.GinContextFromContext(ctx)
15+
gc, err := utils.GinContextFromContext(ctx)
16+
if err != nil {
17+
g.Log.Debug().Err(err).Msg("failed to get gin context")
18+
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
19+
return nil, err
20+
}
1521
res, _, err := g.ServiceProvider.CheckPermissions(ctx, service.MetaFromGin(gc), params)
1622
return res, err
1723
}

internal/graphql/deactivate_account.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ import (
44
"context"
55

66
"github.com/authorizerdev/authorizer/internal/graph/model"
7+
"github.com/authorizerdev/authorizer/internal/metrics"
78
"github.com/authorizerdev/authorizer/internal/service"
89
"github.com/authorizerdev/authorizer/internal/utils"
910
)
1011

1112
// DeactivateAccount delegates to the transport-agnostic service layer.
1213
// Permissions: authenticated user.
1314
func (g *graphqlProvider) DeactivateAccount(ctx context.Context) (*model.Response, error) {
14-
gc, _ := utils.GinContextFromContext(ctx)
15+
gc, err := utils.GinContextFromContext(ctx)
16+
if err != nil {
17+
g.Log.Debug().Err(err).Msg("failed to get gin context")
18+
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
19+
return nil, err
20+
}
1521
res, side, err := g.ServiceProvider.DeactivateAccount(ctx, service.MetaFromGin(gc))
1622
if err != nil {
1723
return nil, err

internal/graphql/delete_email_template.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/authorizerdev/authorizer/internal/graph/model"
7+
"github.com/authorizerdev/authorizer/internal/metrics"
78
"github.com/authorizerdev/authorizer/internal/service"
89
"github.com/authorizerdev/authorizer/internal/utils"
910
)
@@ -13,7 +14,12 @@ import (
1314
//
1415
// Permissions: authorizer:admin
1516
func (g *graphqlProvider) DeleteEmailTemplate(ctx context.Context, params *model.DeleteEmailTemplateRequest) (*model.Response, error) {
16-
gc, _ := utils.GinContextFromContext(ctx)
17+
gc, err := utils.GinContextFromContext(ctx)
18+
if err != nil {
19+
g.Log.Debug().Err(err).Msg("failed to get gin context")
20+
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
21+
return nil, err
22+
}
1723
res, _, err := g.adminService().DeleteEmailTemplate(ctx, service.MetaFromGin(gc), params)
1824
return res, err
1925
}

0 commit comments

Comments
 (0)