• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

gatewayd-io / gatewayd-plugin-auth / 22080082972

16 Feb 2026 10:57PM UTC coverage: 51.923%. First build
22080082972

push

github

mostafa
Add CI workflows, linter config, and fix all lint issues

- Add GitHub Actions workflows for test, release, and signed commits
- Add .golangci.yaml with enable-all linter configuration
- Add build-release and build-platform targets to Makefile
- Fix all 38 golangci-lint issues across the codebase:
  - Extract constants for action strings, magic numbers, and static errors
  - Rename PluginConfigValues to ConfigValues to avoid stutter
  - Pass context through all handler methods (contextcheck)
  - Use proto getters instead of direct field access (protogetter)
  - Fix import ordering (gci), comment formatting (godot)
  - Use non-deprecated GetStoredCredentialsWithError API
  - Remove local SDK replace directive from go.mod

40 of 77 new or added lines in 5 files covered. (51.95%)

459 of 884 relevant lines covered (51.92%)

5.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

41.5
/plugin/auth_handler.go
1
package plugin
2

3
import (
4
        "context"
5
        "encoding/base64"
6
        "fmt"
7

8
        v1 "github.com/gatewayd-io/gatewayd-plugin-sdk/plugin/v1"
9
        "github.com/hashicorp/go-hclog"
10
        "github.com/spf13/cast"
11
)
12

13
// AuthHandler is the central auth state machine dispatcher.
14
// It coordinates credential lookup, authenticator selection, and session management.
15
type AuthHandler struct {
16
        Logger         hclog.Logger
17
        Sessions       *SessionManager
18
        CredStore      CredentialStore
19
        Authenticators map[AuthType]Authenticator
20
        Authorizer     *Authorizer
21
        DefaultAuth    AuthType
22
}
23

24
// NewAuthHandler creates a new AuthHandler with the given components.
25
func NewAuthHandler(
26
        logger hclog.Logger,
27
        sessions *SessionManager,
28
        credStore CredentialStore,
29
        authorizer *Authorizer,
30
        defaultAuth AuthType,
31
        serverVersion string,
32
) *AuthHandler {
7✔
33
        authenticators := map[AuthType]Authenticator{
7✔
34
                AuthCleartext:   &CleartextAuthenticator{ServerVersion: serverVersion},
7✔
35
                AuthMD5:         &MD5Authenticator{ServerVersion: serverVersion},
7✔
36
                AuthScramSHA256: &ScramAuthenticator{ServerVersion: serverVersion},
7✔
37
        }
7✔
38

7✔
39
        return &AuthHandler{
7✔
40
                Logger:         logger,
7✔
41
                Sessions:       sessions,
7✔
42
                CredStore:      credStore,
7✔
43
                Authenticators: authenticators,
7✔
44
                Authorizer:     authorizer,
7✔
45
                DefaultAuth:    defaultAuth,
7✔
46
        }
7✔
47
}
7✔
48

49
// HandleTrafficFromClient processes a client message through the auth state machine.
50
// Returns the (possibly modified) request struct.
51
func (h *AuthHandler) HandleTrafficFromClient(ctx context.Context, req *v1.Struct) (*v1.Struct, error) {
12✔
52
        clientRemote := getClientRemote(req)
12✔
53
        if clientRemote == "" {
12✔
54
                h.Logger.Warn("No client remote address in request")
×
55
                return req, nil
×
56
        }
×
57

58
        session := h.Sessions.GetOrCreate(clientRemote)
12✔
59

12✔
60
        // If already authenticated, check authorization for queries.
12✔
61
        if session.State == StateAuthenticated {
13✔
62
                return h.handleAuthorizedTraffic(ctx, req, session)
1✔
63
        }
1✔
64

65
        fields := req.GetFields()
11✔
66

11✔
67
        // Handle startup message (initial connection).
11✔
68
        if val, exists := fields[FieldStartupMessage]; exists {
18✔
69
                return h.handleStartupMessage(ctx, req, session, val.GetStringValue())
7✔
70
        }
7✔
71

72
        // Handle password message (cleartext or MD5 response).
73
        if val, exists := fields[FieldPasswordMessage]; exists {
8✔
74
                return h.handlePasswordMessage(ctx, req, session, clientRemote, val.GetStringValue())
4✔
75
        }
4✔
76

77
        // Handle SASL initial response (SCRAM client-first).
NEW
78
        if val, exists := fields[FieldSASLInitialResponse]; exists {
×
NEW
79
                return h.handleSASLInitialResponse(ctx, req, session, clientRemote, val.GetStringValue())
×
80
        }
×
81

82
        // Handle SASL response (SCRAM client-final).
NEW
83
        if val, exists := fields[FieldSASLResponse]; exists {
×
NEW
84
                return h.handleSASLResponse(ctx, req, session, clientRemote, val.GetStringValue())
×
85
        }
×
86

87
        // Not an auth-related message and not authenticated yet -- this shouldn't happen
88
        // in normal flow. Pass through (GatewayD may have its own handling).
89
        return req, nil
×
90
}
91

92
// handleStartupMessage processes a PostgreSQL StartupMessage.
93
func (h *AuthHandler) handleStartupMessage(
94
        ctx context.Context, req *v1.Struct, session *Session, encodedMsg string,
95
) (*v1.Struct, error) {
7✔
96
        decoded, err := DecodeBase64Field(encodedMsg)
7✔
97
        if err != nil {
7✔
98
                h.Logger.Error("Failed to decode startup message", "error", err)
×
99
                return h.terminateWithAuthFail(req, "invalid startup message")
×
100
        }
×
101

102
        user, database, err := ParseStartupParams(decoded)
7✔
103
        if err != nil {
7✔
104
                h.Logger.Error("Failed to parse startup parameters", "error", err)
×
105
                return h.terminateWithAuthFail(req, "invalid startup parameters")
×
106
        }
×
107

108
        h.Logger.Debug("Startup message", "user", user, "database", database)
7✔
109

7✔
110
        // Look up user in credential store.
7✔
111
        cred, err := h.CredStore.LookupUser(ctx, user)
7✔
112
        if err != nil {
8✔
113
                h.Logger.Info("User lookup failed", "user", user, "error", err)
1✔
114
                return h.terminateWithAuthFail(req,
1✔
115
                        fmt.Sprintf("password authentication failed for user %q", user))
1✔
116
        }
1✔
117

118
        // Check if database is allowed.
119
        if !cred.IsDatabaseAllowed(database) {
7✔
120
                h.Logger.Info("Database not allowed", "user", user, "database", database)
1✔
121
                return h.terminateWithAuthFail(req,
1✔
122
                        fmt.Sprintf("user %q is not allowed to connect to database %q", user, database))
1✔
123
        }
1✔
124

125
        session.Username = user
5✔
126
        session.Database = database
5✔
127
        session.Roles = cred.Roles
5✔
128

5✔
129
        // Select auth method.
5✔
130
        authMethod := h.selectAuthMethod(cred)
5✔
131
        authenticator, ok := h.Authenticators[authMethod]
5✔
132
        if !ok {
5✔
133
                h.Logger.Error("No authenticator for method", "method", authMethod)
×
134
                return h.terminateWithAuthFail(req, "unsupported auth method")
×
135
        }
×
136

137
        // Send auth challenge.
138
        challenge, err := authenticator.HandleStartup(session, cred)
5✔
139
        if err != nil {
5✔
140
                h.Logger.Error("Failed to create auth challenge", "error", err)
×
141
                return h.terminateWithAuthFail(req, "internal error during authentication")
×
142
        }
×
143

144
        return sendTerminateResponse(req, challenge, h.Logger)
5✔
145
}
146

147
// handlePasswordMessage processes a PasswordMessage (cleartext or MD5 response).
148
func (h *AuthHandler) handlePasswordMessage(
149
        ctx context.Context, req *v1.Struct, session *Session, clientRemote, encodedMsg string,
150
) (*v1.Struct, error) {
4✔
151
        if session.State != StateChallengeSent {
4✔
152
                h.Logger.Warn("Password message in unexpected state", "state", session.State)
×
153
                return h.terminateWithAuthFail(req, "unexpected password message")
×
154
        }
×
155

156
        decoded, err := DecodeBase64Field(encodedMsg)
4✔
157
        if err != nil {
4✔
158
                h.Logger.Error("Failed to decode password message", "error", err)
×
159
                return h.terminateWithAuthFail(req, "invalid password message")
×
160
        }
×
161

162
        msgData := ParsePasswordMessage(decoded)
4✔
163

4✔
164
        cred, err := h.CredStore.LookupUser(ctx, session.Username)
4✔
165
        if err != nil {
4✔
166
                h.Logger.Error("User lookup failed during password validation", "error", err)
×
167
                h.Sessions.Remove(clientRemote)
×
168
                return h.terminateWithAuthFail(req, "authentication failed")
×
169
        }
×
170

171
        authenticator, ok := h.Authenticators[session.AuthMethod]
4✔
172
        if !ok {
4✔
173
                h.Sessions.Remove(clientRemote)
×
174
                return h.terminateWithAuthFail(req, "unsupported auth method")
×
175
        }
×
176

177
        response, authenticated, err := authenticator.HandleResponse(session, cred, msgData)
4✔
178
        if err != nil {
4✔
179
                h.Logger.Debug("Auth response handling error", "error", err)
×
180
        }
×
181

182
        if !authenticated {
5✔
183
                h.Logger.Info("Authentication failed", "user", session.Username)
1✔
184
                h.Sessions.Remove(clientRemote)
1✔
185
                AuthFailures.Inc()
1✔
186
                return sendTerminateResponse(req, response, h.Logger)
1✔
187
        }
1✔
188

189
        h.Logger.Info("Authentication successful", "user", session.Username)
3✔
190
        AuthSuccesses.Inc()
3✔
191
        return sendTerminateResponse(req, response, h.Logger)
3✔
192
}
193

194
// handleSASLInitialResponse processes a SASLInitialResponse (SCRAM client-first).
195
func (h *AuthHandler) handleSASLInitialResponse(
196
        ctx context.Context, req *v1.Struct, session *Session, clientRemote, encodedMsg string,
197
) (*v1.Struct, error) {
×
198
        if session.State != StateChallengeSent || session.AuthMethod != AuthScramSHA256 {
×
199
                h.Logger.Warn("SASL initial response in unexpected state")
×
200
                return h.terminateWithAuthFail(req, "unexpected SASL message")
×
201
        }
×
202

203
        decoded, err := DecodeBase64Field(encodedMsg)
×
204
        if err != nil {
×
205
                h.Logger.Error("Failed to decode SASL initial response", "error", err)
×
206
                return h.terminateWithAuthFail(req, "invalid SASL message")
×
207
        }
×
208

209
        msgData := cast.ToStringMapString(string(decoded))
×
210

×
NEW
211
        cred, err := h.CredStore.LookupUser(ctx, session.Username)
×
212
        if err != nil {
×
213
                h.Sessions.Remove(clientRemote)
×
214
                return h.terminateWithAuthFail(req, "authentication failed")
×
215
        }
×
216

217
        authenticator := h.Authenticators[AuthScramSHA256]
×
218
        response, authenticated, err := authenticator.HandleResponse(session, cred, msgData)
×
219
        if err != nil {
×
220
                h.Logger.Debug("SASL initial response error", "error", err)
×
221
        }
×
222

223
        if authenticated {
×
224
                // Shouldn't happen at this stage, but handle it.
×
225
                AuthSuccesses.Inc()
×
226
        }
×
227

228
        return sendTerminateResponse(req, response, h.Logger)
×
229
}
230

231
// handleSASLResponse processes a SASLResponse (SCRAM client-final).
232
func (h *AuthHandler) handleSASLResponse(
233
        ctx context.Context, req *v1.Struct, session *Session, clientRemote, encodedMsg string,
234
) (*v1.Struct, error) {
×
235
        if session.State != StateScramContinue {
×
236
                h.Logger.Warn("SASL response in unexpected state")
×
237
                return h.terminateWithAuthFail(req, "unexpected SASL message")
×
238
        }
×
239

240
        decoded, err := DecodeBase64Field(encodedMsg)
×
241
        if err != nil {
×
242
                h.Logger.Error("Failed to decode SASL response", "error", err)
×
243
                return h.terminateWithAuthFail(req, "invalid SASL message")
×
244
        }
×
245

246
        msgData := cast.ToStringMapString(string(decoded))
×
247

×
NEW
248
        cred, err := h.CredStore.LookupUser(ctx, session.Username)
×
249
        if err != nil {
×
250
                h.Sessions.Remove(clientRemote)
×
251
                return h.terminateWithAuthFail(req, "authentication failed")
×
252
        }
×
253

254
        authenticator := h.Authenticators[AuthScramSHA256]
×
255
        response, authenticated, err := authenticator.HandleResponse(session, cred, msgData)
×
256
        if err != nil {
×
257
                h.Logger.Debug("SASL response error", "error", err)
×
258
        }
×
259

260
        if !authenticated {
×
261
                h.Logger.Info("SCRAM authentication failed", "user", session.Username)
×
262
                h.Sessions.Remove(clientRemote)
×
263
                AuthFailures.Inc()
×
264
                return sendTerminateResponse(req, response, h.Logger)
×
265
        }
×
266

267
        h.Logger.Info("SCRAM authentication successful", "user", session.Username)
×
268
        AuthSuccesses.Inc()
×
269
        return sendTerminateResponse(req, response, h.Logger)
×
270
}
271

272
// handleAuthorizedTraffic checks authorization for an already-authenticated session.
273
func (h *AuthHandler) handleAuthorizedTraffic(
274
        _ context.Context, req *v1.Struct, session *Session,
275
) (*v1.Struct, error) {
1✔
276
        if h.Authorizer == nil {
2✔
277
                // No authorizer configured, pass through.
1✔
278
                return req, nil
1✔
279
        }
1✔
280

NEW
281
        fields := req.GetFields()
×
NEW
282

×
283
        // Check if this is a query message that needs authorization.
×
284
        var query string
×
NEW
285
        if val, exists := fields[FieldQuery]; exists {
×
286
                decoded, err := base64.StdEncoding.DecodeString(val.GetStringValue())
×
287
                if err == nil {
×
288
                        queryData := cast.ToStringMapString(string(decoded))
×
289
                        query = queryData["String"]
×
290
                }
×
291
        }
NEW
292
        if val, exists := fields[FieldParse]; exists && query == "" {
×
293
                decoded, err := base64.StdEncoding.DecodeString(val.GetStringValue())
×
294
                if err == nil {
×
295
                        parseData := cast.ToStringMapString(string(decoded))
×
296
                        query = parseData["Query"]
×
297
                }
×
298
        }
299

300
        if query == "" {
×
301
                // Not a query message, pass through.
×
302
                return req, nil
×
303
        }
×
304

305
        allowed, err := h.Authorizer.Authorize(session.Username, session.Database, query)
×
306
        if err != nil {
×
307
                h.Logger.Error("Authorization check failed", "error", err)
×
308
                // Fail open on errors (could also fail closed -- configurable in the future).
×
309
                return req, nil
×
310
        }
×
311

312
        if !allowed {
×
313
                h.Logger.Info("Query denied",
×
314
                        "user", session.Username,
×
315
                        "database", session.Database,
×
316
                        "query", query)
×
317
                AuthzDenials.Inc()
×
318

×
319
                response, buildErr := BuildAccessDeniedResponse(
×
320
                        fmt.Sprintf("permission denied for user %q", session.Username))
×
321
                if buildErr != nil {
×
322
                        h.Logger.Error("Failed to build access denied response", "error", buildErr)
×
323
                        return req, nil
×
324
                }
×
325
                return sendTerminateResponse(req, response, h.Logger)
×
326
        }
327

328
        return req, nil
×
329
}
330

331
// selectAuthMethod selects the best auth method for a user.
332
func (h *AuthHandler) selectAuthMethod(cred *UserCredential) AuthType {
5✔
333
        // If the user supports the default, use it.
5✔
334
        if cred.SupportsAuthMethod(h.DefaultAuth) {
10✔
335
                return h.DefaultAuth
5✔
336
        }
5✔
337
        // Otherwise, use the first method they support.
338
        if len(cred.AuthMethods) > 0 {
×
339
                return AuthType(cred.AuthMethods[0])
×
340
        }
×
341
        return h.DefaultAuth
×
342
}
343

344
// terminateWithAuthFail builds and returns an auth failure response.
345
func (h *AuthHandler) terminateWithAuthFail(req *v1.Struct, detail string) (*v1.Struct, error) {
2✔
346
        response, err := BuildAuthFailResponse(detail)
2✔
347
        if err != nil {
2✔
348
                h.Logger.Error("Failed to build auth fail response", "error", err)
×
349
                return req, nil
×
350
        }
×
351
        return sendTerminateResponse(req, response, h.Logger)
2✔
352
}
353

354
// getClientRemote extracts the client's remote address from the request.
355
// GatewayD passes client info as a nested struct: {"client": {"local": "...", "remote": "..."}}.
356
func getClientRemote(req *v1.Struct) string {
12✔
357
        val, exists := req.GetFields()["client"]
12✔
358
        if !exists {
12✔
359
                return ""
×
360
        }
×
361

362
        // The client field is a StructValue (nested map), not a string.
363
        if clientStruct := val.GetStructValue(); clientStruct != nil {
12✔
NEW
364
                if remote, ok := clientStruct.GetFields()["remote"]; ok {
×
365
                        return remote.GetStringValue()
×
366
                }
×
367
        }
368

369
        // Fallback: try as a string map (for compatibility).
370
        clientMap := cast.ToStringMap(val.GetStringValue())
12✔
371
        return cast.ToString(clientMap["remote"])
12✔
372
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc