From 361921b7de6ab9dabb4049ed70bb7dddf16dc8b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:31:06 +0000 Subject: [PATCH 1/5] Initial plan From 5fc4f4b14ce48ca8625e96469b130dfa01ca2299 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:35:20 +0000 Subject: [PATCH 2/5] Add AuthVerbose flag to control authentication logging verbosity Co-authored-by: awagner-mainz <2088443+awagner-mainz@users.noreply.github.com> --- internal/auth/authenticate.go | 100 +++++++++++++++++++++++----------- internal/models/options.go | 19 ++++--- main.go | 5 +- 3 files changed, 82 insertions(+), 42 deletions(-) diff --git a/internal/auth/authenticate.go b/internal/auth/authenticate.go index e47ae37..46a5b58 100644 --- a/internal/auth/authenticate.go +++ b/internal/auth/authenticate.go @@ -49,13 +49,13 @@ var Config = map[string]*huma.SecurityScheme{ }, } -// APITermination returns a middleware function that evaluates if any of the preceding +// AuthTermination returns a middleware function that evaluates if any of the preceding // // authentication middleware functions were successful. If not, it rejects the request, // otherwise it calls the next middleware (or the final handler) function. // This is supposed to be called as the last auth middleware function in // the chain. -func AuthTermination(api huma.API) func(ctx huma.Context, next func(huma.Context)) { +func AuthTermination(api huma.API, options *models.Options) func(ctx huma.Context, next func(huma.Context)) { return func(ctx huma.Context, next func(huma.Context)) { // Check if the current operation requires authentication isAuthRequired := false @@ -77,7 +77,9 @@ func AuthTermination(api huma.API) func(ctx huma.Context, next func(huma.Context next(ctx) return } - fmt.Print(" Authentication failed.\n") + if options.AuthVerbose { + fmt.Print(" Authentication failed.\n") + } _ = huma.WriteErr(api, ctx, http.StatusUnauthorized, "Authentication failed. Perhaps a missing or incorrect API key?") } } @@ -107,7 +109,9 @@ func EmbAPIKeyAdminAuth(api huma.API, options *models.Options) func(ctx huma.Con if token == options.AdminKey { ctx = huma.WithValue(ctx, IsAdminKey, true) ctx = huma.WithValue(ctx, AuthUserKey, "admin") - fmt.Print(" Admin authentication successful\n") + if options.AuthVerbose { + fmt.Print(" Admin authentication successful\n") + } next(ctx) return } @@ -167,7 +171,9 @@ func EmbAPIKeyOwnerAuth(api huma.API, pool *pgxpool.Pool, options *models.Option if EmbAPIKeyIsValid(token, storedHash) { ctx = huma.WithValue(ctx, IsOwnerKey, true) ctx = huma.WithValue(ctx, AuthUserKey, owner) - fmt.Printf(" Owner authentication successful: %s\n", owner) + if options.AuthVerbose { + fmt.Printf(" Owner authentication successful: %s\n", owner) + } next(ctx) return } @@ -213,27 +219,35 @@ func EmbAPIKeyReaderAuth(api huma.API, pool *pgxpool.Pool, options *models.Optio return } - fmt.Printf(" Reader auth for owner=%s project=%s definition=%s instance=%s running...\n", owner, project, definition, instance) + if options.AuthVerbose { + fmt.Printf(" Reader auth for owner=%s project=%s definition=%s instance=%s running...\n", owner, project, definition, instance) + } // Branch based on whether project, definition, or instance is being accessed if len(project) > 0 { - fmt.Print(" Checking project access...\n") - handleProjectReaderAuth(api, pool, owner, project)(ctx, next) + if options.AuthVerbose { + fmt.Print(" Checking project access...\n") + } + handleProjectReaderAuth(api, pool, owner, project, options)(ctx, next) return } if len(definition) > 0 { - fmt.Print(" Checking definition access...\n") - handleDefinitionReaderAuth(api, pool, owner, definition)(ctx, next) + if options.AuthVerbose { + fmt.Print(" Checking definition access...\n") + } + handleDefinitionReaderAuth(api, pool, owner, definition, options)(ctx, next) return } if len(instance) > 0 { - fmt.Print(" Checking instance access...\n") - handleInstanceReaderAuth(api, pool, owner, instance)(ctx, next) + if options.AuthVerbose { + fmt.Print(" Checking instance access...\n") + } + handleInstanceReaderAuth(api, pool, owner, instance, options)(ctx, next) return } } } -func handleProjectReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, project string) func(ctx huma.Context, next func(huma.Context)) { +func handleProjectReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, project string, options *models.Options) func(ctx huma.Context, next func(huma.Context)) { { return func(ctx huma.Context, next func(huma.Context)) { @@ -249,7 +263,9 @@ func handleProjectReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, pro // If project exists and public_read is true, allow unauthenticated access if err == nil && publicRead.Valid && publicRead.Bool { // Public read is enabled, allow unauthenticated access - fmt.Print(" Public read access granted (no authentication required)\n") + if options.AuthVerbose { + fmt.Print(" Public read access granted (no authentication required)\n") + } ctx = huma.WithValue(ctx, AuthUserKey, "public") next(ctx) return @@ -278,7 +294,9 @@ func handleProjectReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, pro storedHash := authKey.EmbAPIKey if EmbAPIKeyIsValid(token, storedHash) { - fmt.Print(" Reader authentication successful\n") + if options.AuthVerbose { + fmt.Print(" Reader authentication successful\n") + } ctx = huma.WithValue(ctx, AuthUserKey, authKey.UserHandle) next(ctx) return @@ -290,7 +308,7 @@ func handleProjectReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, pro } } -func handleDefinitionReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, definition string) func(ctx huma.Context, next func(huma.Context)) { +func handleDefinitionReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, definition string, options *models.Options) func(ctx huma.Context, next func(huma.Context)) { { return func(ctx huma.Context, next func(huma.Context)) { @@ -317,7 +335,9 @@ func handleDefinitionReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, storedHash := authKey.EmbAPIKey if EmbAPIKeyIsValid(token, storedHash) { - fmt.Print(" Reader authentication successful\n") + if options.AuthVerbose { + fmt.Print(" Reader authentication successful\n") + } ctx = huma.WithValue(ctx, AuthUserKey, authKey.UserHandle) next(ctx) return @@ -329,7 +349,7 @@ func handleDefinitionReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, } } -func handleInstanceReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, instance string) func(ctx huma.Context, next func(huma.Context)) { +func handleInstanceReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, instance string, options *models.Options) func(ctx huma.Context, next func(huma.Context)) { { return func(ctx huma.Context, next func(huma.Context)) { @@ -356,7 +376,9 @@ func handleInstanceReaderAuth(api huma.API, pool *pgxpool.Pool, owner string, in storedHash := authKey.EmbAPIKey if EmbAPIKeyIsValid(token, storedHash) { - fmt.Print(" Reader authentication successful\n") + if options.AuthVerbose { + fmt.Print(" Reader authentication successful\n") + } ctx = huma.WithValue(ctx, AuthUserKey, authKey.UserHandle) next(ctx) return @@ -406,27 +428,35 @@ func EmbAPIKeyEditorAuth(api huma.API, pool *pgxpool.Pool, options *models.Optio return } - fmt.Printf(" Editor auth for owner=%s project=%s definition=%s instance=%s running...\n", owner, project, definition, instance) + if options.AuthVerbose { + fmt.Printf(" Editor auth for owner=%s project=%s definition=%s instance=%s running...\n", owner, project, definition, instance) + } // Branch based on whether project, definition, or instance is being accessed if len(project) > 0 { - fmt.Print(" Checking project editor access...\n") - handleProjectEditorAuth(api, pool, owner, project)(ctx, next) + if options.AuthVerbose { + fmt.Print(" Checking project editor access...\n") + } + handleProjectEditorAuth(api, pool, owner, project, options)(ctx, next) return } if len(definition) > 0 { - fmt.Print(" Checking definition editor access...\n") - handleDefinitionEditorAuth(api, pool, owner, definition)(ctx, next) + if options.AuthVerbose { + fmt.Print(" Checking definition editor access...\n") + } + handleDefinitionEditorAuth(api, pool, owner, definition, options)(ctx, next) return } if len(instance) > 0 { - fmt.Print(" Checking instance editor access...\n") - handleInstanceEditorAuth(api, pool, owner, instance)(ctx, next) + if options.AuthVerbose { + fmt.Print(" Checking instance editor access...\n") + } + handleInstanceEditorAuth(api, pool, owner, instance, options)(ctx, next) return } } } -func handleProjectEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, project string) func(ctx huma.Context, next func(huma.Context)) { +func handleProjectEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, project string, options *models.Options) func(ctx huma.Context, next func(huma.Context)) { return func(ctx huma.Context, next func(huma.Context)) { token := strings.TrimPrefix(ctx.Header("Authorization"), "Bearer ") @@ -455,7 +485,9 @@ func handleProjectEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, pro storedHash := authKey.EmbAPIKey if EmbAPIKeyIsValid(token, storedHash) { - fmt.Printf(" Editor authentication successful (role: %s)\n", authKey.Role) + if options.AuthVerbose { + fmt.Printf(" Editor authentication successful (role: %s)\n", authKey.Role) + } ctx = huma.WithValue(ctx, AuthUserKey, authKey.UserHandle) next(ctx) return @@ -466,7 +498,7 @@ func handleProjectEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, pro } } -func handleDefinitionEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, definition string) func(ctx huma.Context, next func(huma.Context)) { +func handleDefinitionEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, definition string, options *models.Options) func(ctx huma.Context, next func(huma.Context)) { return func(ctx huma.Context, next func(huma.Context)) { token := strings.TrimPrefix(ctx.Header("Authorization"), "Bearer ") @@ -493,7 +525,9 @@ func handleDefinitionEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, storedHash := authKey.EmbAPIKey if EmbAPIKeyIsValid(token, storedHash) { - fmt.Print(" Editor authentication successful\n") + if options.AuthVerbose { + fmt.Print(" Editor authentication successful\n") + } ctx = huma.WithValue(ctx, AuthUserKey, authKey.UserHandle) next(ctx) return @@ -504,7 +538,7 @@ func handleDefinitionEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, } } -func handleInstanceEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, instance string) func(ctx huma.Context, next func(huma.Context)) { +func handleInstanceEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, instance string, options *models.Options) func(ctx huma.Context, next func(huma.Context)) { return func(ctx huma.Context, next func(huma.Context)) { token := strings.TrimPrefix(ctx.Header("Authorization"), "Bearer ") @@ -533,7 +567,9 @@ func handleInstanceEditorAuth(api huma.API, pool *pgxpool.Pool, owner string, in storedHash := authKey.EmbAPIKey if EmbAPIKeyIsValid(token, storedHash) { - fmt.Printf(" Editor authentication successful (role: %s)\n", authKey.Role) + if options.AuthVerbose { + fmt.Printf(" Editor authentication successful (role: %s)\n", authKey.Role) + } ctx = huma.WithValue(ctx, AuthUserKey, authKey.UserHandle) next(ctx) return diff --git a/internal/models/options.go b/internal/models/options.go index 59d02ba..be8bd14 100644 --- a/internal/models/options.go +++ b/internal/models/options.go @@ -2,13 +2,14 @@ package models // Options for the CLI. type Options struct { - Debug bool ` env:"SERVICE_DEBUG" doc:"Enable debug logging" short:"d" default:"true"` - Host string ` env:"SERVICE_HOST" doc:"Hostname to listen on" default:"localhost"` - Port int ` env:"SERVICE_PORT" doc:"Port to listen on" short:"p" default:"8880"` - DBHost string `name:"db-host" env:"SERVICE_DBHOST" doc:"Database hostname" default:"localhost"` - DBPort int `name:"db-port" env:"SERVICE_DBPORT" doc:"Database port" default:"5432"` - DBUser string `name:"db-user" env:"SERVICE_DBUSER" doc:"Database username" default:"postgres"` - DBPassword string `name:"db-password" env:"SERVICE_DBPASSWORD" doc:"Database password" default:"password"` - DBName string `name:"db-name" env:"SERVICE_DBNAME" doc:"Database name" default:"postgres"` - AdminKey string `name:"admin-key" env:"SERVICE_ADMINKEY" doc:"Admin API key"` + Debug bool ` env:"SERVICE_DEBUG" doc:"Enable debug logging" short:"d" default:"true"` + AuthVerbose bool `name:"auth-verbose" env:"SERVICE_AUTH_VERBOSE" doc:"Enable verbose authentication logging" short:"v" default:"false"` + Host string ` env:"SERVICE_HOST" doc:"Hostname to listen on" default:"localhost"` + Port int ` env:"SERVICE_PORT" doc:"Port to listen on" short:"p" default:"8880"` + DBHost string `name:"db-host" env:"SERVICE_DBHOST" doc:"Database hostname" default:"localhost"` + DBPort int `name:"db-port" env:"SERVICE_DBPORT" doc:"Database port" default:"5432"` + DBUser string `name:"db-user" env:"SERVICE_DBUSER" doc:"Database username" default:"postgres"` + DBPassword string `name:"db-password" env:"SERVICE_DBPASSWORD" doc:"Database password" default:"password"` + DBName string `name:"db-name" env:"SERVICE_DBNAME" doc:"Database name" default:"postgres"` + AdminKey string `name:"admin-key" env:"SERVICE_ADMINKEY" doc:"Admin API key"` } diff --git a/main.go b/main.go index 0e925c9..d85dc7b 100644 --- a/main.go +++ b/main.go @@ -35,6 +35,9 @@ func main() { if os.Getenv("SERVICE_DEBUG") != "" { options.Debug = os.Getenv("SERVICE_DEBUG") == "true" } + if os.Getenv("SERVICE_AUTH_VERBOSE") != "" { + options.AuthVerbose = os.Getenv("SERVICE_AUTH_VERBOSE") == "true" + } if os.Getenv("SERVICE_HOST") != "" { options.Host = os.Getenv("SERVICE_HOST") } @@ -112,7 +115,7 @@ func main() { api.UseMiddleware(auth.EmbAPIKeyOwnerAuth(api, pool, options)) api.UseMiddleware(auth.EmbAPIKeyEditorAuth(api, pool, options)) api.UseMiddleware(auth.EmbAPIKeyReaderAuth(api, pool, options)) - api.UseMiddleware(auth.AuthTermination(api)) + api.UseMiddleware(auth.AuthTermination(api, options)) // Add routes to the API err = handlers.AddRoutes(pool, keyGen, api) From 938c69759c509a76edd8bf79d3236d9910f05630 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:37:25 +0000 Subject: [PATCH 3/5] Add comprehensive tests for AuthVerbose flag Co-authored-by: awagner-mainz <2088443+awagner-mainz@users.noreply.github.com> --- internal/auth/auth_verbose_test.go | 229 +++++++++++++++++++++++++++++ internal/handlers/handlers_test.go | 19 +-- 2 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 internal/auth/auth_verbose_test.go diff --git a/internal/auth/auth_verbose_test.go b/internal/auth/auth_verbose_test.go new file mode 100644 index 0000000..8d5639e --- /dev/null +++ b/internal/auth/auth_verbose_test.go @@ -0,0 +1,229 @@ +package auth_test + +import ( + "bytes" + "context" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/adapters/humago" + "github.com/mpilhlt/embapi/internal/auth" + "github.com/mpilhlt/embapi/internal/models" + "github.com/stretchr/testify/assert" +) + +// TestAuthVerboseFlag tests that authentication logging is controlled by the AuthVerbose flag +func TestAuthVerboseFlag(t *testing.T) { + // Set required environment variables for testing + if os.Getenv("ENCRYPTION_KEY") == "" { + os.Setenv("ENCRYPTION_KEY", "test-encryption-key-32-bytes-long-1234567890") + } + + tests := []struct { + name string + authVerbose bool + expectLogs bool + }{ + { + name: "AuthVerbose=true should produce logs", + authVerbose: true, + expectLogs: true, + }, + { + name: "AuthVerbose=false should suppress logs", + authVerbose: false, + expectLogs: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Capture stdout to check for log messages + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Create test options + options := &models.Options{ + AuthVerbose: tt.authVerbose, + AdminKey: "test-admin-key", + } + + // Create a simple API and router + config := huma.DefaultConfig("Test API", "1.0.0") + router := http.NewServeMux() + api := humago.New(router, config) + + // Register auth middlewares + api.UseMiddleware(auth.EmbAPIKeyAdminAuth(api, options)) + api.UseMiddleware(auth.AuthTermination(api, options)) + + // Register a test endpoint that requires authentication + huma.Register(api, huma.Operation{ + OperationID: "test-endpoint", + Method: http.MethodGet, + Path: "/test", + Security: []map[string][]string{ + {"adminAuth": {}}, + }, + }, func(ctx context.Context, input *struct{}) (*struct{}, error) { + return &struct{}{}, nil + }) + + // Test 1: Successful authentication (should log if verbose) + req1 := httptest.NewRequest(http.MethodGet, "/test", nil) + req1.Header.Set("Authorization", "Bearer test-admin-key") + resp1 := httptest.NewRecorder() + router.ServeHTTP(resp1, req1) + + // Capture output + w.Close() + var buf bytes.Buffer + io.Copy(&buf, r) + os.Stdout = oldStdout + output := buf.String() + + // Verify status code (204 No Content is expected for successful operation with no body) + assert.Equal(t, http.StatusNoContent, resp1.Code, "Expected successful authentication") + + // Check for log output based on verbose flag + if tt.expectLogs { + assert.Contains(t, output, "Admin authentication successful", "Expected admin auth log when verbose=true") + } else { + assert.NotContains(t, output, "Admin authentication successful", "Expected no admin auth log when verbose=false") + } + + // Test 2: Failed authentication (should log if verbose) + // Capture stdout again for failed auth test + r2, w2, _ := os.Pipe() + os.Stdout = w2 + + req2 := httptest.NewRequest(http.MethodGet, "/test", nil) + req2.Header.Set("Authorization", "Bearer wrong-key") + resp2 := httptest.NewRecorder() + router.ServeHTTP(resp2, req2) + + // Capture output + w2.Close() + var buf2 bytes.Buffer + io.Copy(&buf2, r2) + os.Stdout = oldStdout + output2 := buf2.String() + + // Verify status code + assert.Equal(t, http.StatusUnauthorized, resp2.Code, "Expected failed authentication") + + // Check for log output based on verbose flag + if tt.expectLogs { + assert.Contains(t, output2, "Authentication failed", "Expected auth failure log when verbose=true") + } else { + assert.NotContains(t, output2, "Authentication failed", "Expected no auth failure log when verbose=false") + } + }) + } +} + +// TestAuthVerboseEnvironmentVariable tests that the environment variable is properly handled +func TestAuthVerboseEnvironmentVariable(t *testing.T) { + tests := []struct { + name string + envValue string + expected bool + }{ + { + name: "SERVICE_AUTH_VERBOSE=true", + envValue: "true", + expected: true, + }, + { + name: "SERVICE_AUTH_VERBOSE=false", + envValue: "false", + expected: false, + }, + { + name: "SERVICE_AUTH_VERBOSE empty", + envValue: "", + expected: false, // default value + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clean up environment + os.Unsetenv("SERVICE_AUTH_VERBOSE") + + if tt.envValue != "" { + os.Setenv("SERVICE_AUTH_VERBOSE", tt.envValue) + defer os.Unsetenv("SERVICE_AUTH_VERBOSE") + } + + options := &models.Options{ + AuthVerbose: false, // default + } + + // Simulate what main.go does + if os.Getenv("SERVICE_AUTH_VERBOSE") != "" { + options.AuthVerbose = os.Getenv("SERVICE_AUTH_VERBOSE") == "true" + } + + assert.Equal(t, tt.expected, options.AuthVerbose, "AuthVerbose should match expected value") + }) + } +} + +// TestNoAuthLogsInQuietMode verifies that with AuthVerbose=false, various auth scenarios don't produce logs +func TestNoAuthLogsInQuietMode(t *testing.T) { + // This test demonstrates the fix for the bulk upload/crawler scenario + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + options := &models.Options{ + AuthVerbose: false, // Quiet mode + AdminKey: "test-admin-key", + } + + config := huma.DefaultConfig("Test API", "1.0.0") + router := http.NewServeMux() + api := humago.New(router, config) + + api.UseMiddleware(auth.EmbAPIKeyAdminAuth(api, options)) + api.UseMiddleware(auth.AuthTermination(api, options)) + + huma.Register(api, huma.Operation{ + OperationID: "bulk-test", + Method: http.MethodPost, + Path: "/bulk", + Security: []map[string][]string{ + {"adminAuth": {}}, + }, + }, func(ctx context.Context, input *struct{}) (*struct{}, error) { + return &struct{}{}, nil + }) + + // Simulate many requests (like bulk upload or crawler) + for i := 0; i < 50; i++ { + req := httptest.NewRequest(http.MethodPost, "/bulk", nil) + req.Header.Set("Authorization", "Bearer test-admin-key") + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + assert.Equal(t, http.StatusNoContent, resp.Code) + } + + // Capture output + w.Close() + var buf bytes.Buffer + io.Copy(&buf, r) + os.Stdout = oldStdout + output := buf.String() + + // Count authentication success messages - should be 0 in quiet mode + count := strings.Count(output, "authentication successful") + assert.Equal(t, 0, count, "Expected no authentication logs in quiet mode during bulk operations") +} diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index 6ac318e..f9666dd 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -49,14 +49,15 @@ import ( var ( options = models.Options{ - Debug: true, - Host: "localhost", - Port: 8080, - DBHost: "localhost", - DBName: "testdb", - DBUser: "test", - DBPassword: "test", - AdminKey: "Password123", + Debug: true, + AuthVerbose: false, // Set to false for cleaner test output + Host: "localhost", + Port: 8080, + DBHost: "localhost", + DBName: "testdb", + DBUser: "test", + DBPassword: "test", + AdminKey: "Password123", } connPool *pgxpool.Pool teardown func() @@ -188,7 +189,7 @@ func startTestServer(t *testing.T, pool *pgxpool.Pool, keyGen handlers.RandomKey api.UseMiddleware(auth.EmbAPIKeyOwnerAuth(api, pool, &options)) api.UseMiddleware(auth.EmbAPIKeyEditorAuth(api, pool, &options)) api.UseMiddleware(auth.EmbAPIKeyReaderAuth(api, pool, &options)) - api.UseMiddleware(auth.AuthTermination(api)) + api.UseMiddleware(auth.AuthTermination(api, &options)) err := handlers.AddRoutes(pool, keyGen, api) if err != nil { From 6fa8ba31a8027c5fb772addf611b42c938bc9197 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:39:19 +0000 Subject: [PATCH 4/5] Update documentation with AuthVerbose configuration Co-authored-by: awagner-mainz <2088443+awagner-mainz@users.noreply.github.com> --- .../deployment/environment-variables.md | 49 +++++++++++++++++++ docs/content/getting-started/configuration.md | 29 +++++++++++ template.env | 1 + 3 files changed, 79 insertions(+) diff --git a/docs/content/deployment/environment-variables.md b/docs/content/deployment/environment-variables.md index 459ee5d..f8c157c 100644 --- a/docs/content/deployment/environment-variables.md +++ b/docs/content/deployment/environment-variables.md @@ -108,6 +108,51 @@ SERVICE_DEBUG=true # Development (verbose) - `true`: Detailed logs, useful for debugging - `false`: Minimal logs, recommended for production +### SERVICE_AUTH_VERBOSE + +Enable verbose authentication logging. + +- **Type:** Boolean +- **Required:** No +- **Default:** `false` +- **Environment Variable:** `SERVICE_AUTH_VERBOSE` +- **Command-line Flag:** `-v`, `--auth-verbose` + +**Description:** Controls authentication log output. When enabled, logs all authentication attempts including successful and failed authentications. When disabled (default), authentication operates silently. + +**Example:** + +```bash +SERVICE_AUTH_VERBOSE=false # Quiet mode (default, recommended for production) +SERVICE_AUTH_VERBOSE=true # Verbose mode (useful for debugging) +``` + +**Use Cases:** + +- **`true` (verbose mode):** + - Debugging authentication issues + - Monitoring security events + - Development and testing + - Auditing access patterns + +- **`false` (quiet mode, recommended):** + - Production environments + - Bulk upload operations + - High-traffic scenarios + - When crawlers or automated tools access the API frequently + +**Example Log Output (when `true`):** + +``` + Owner authentication successful: alice + Reader auth for owner=bob project=test definition= instance= running... + Checking project access... + Reader authentication successful + Authentication failed. +``` + +**Note:** Authentication still functions normally when logging is disabled; only the console output is suppressed. This prevents log spam during bulk operations or high-traffic periods. + ### SERVICE_HOST Hostname or IP address to bind the service to. @@ -275,6 +320,7 @@ SERVICE_DBNAME=embapi_test # Testing database ```bash # .env file for development SERVICE_DEBUG=true +SERVICE_AUTH_VERBOSE=true SERVICE_HOST=localhost SERVICE_PORT=8880 SERVICE_DBHOST=localhost @@ -291,6 +337,7 @@ ENCRYPTION_KEY=dev-encryption-key-min-32-chars-long ```bash # .env file for Docker Compose SERVICE_DEBUG=false +SERVICE_AUTH_VERBOSE=false SERVICE_HOST=0.0.0.0 SERVICE_PORT=8880 SERVICE_DBHOST=postgres @@ -307,6 +354,7 @@ ENCRYPTION_KEY=generated_encryption_key_from_setup_script ```bash # .env file for production (or use secrets management) SERVICE_DEBUG=false +SERVICE_AUTH_VERBOSE=false SERVICE_HOST=0.0.0.0 SERVICE_PORT=8880 SERVICE_DBHOST=db.internal.example.com @@ -384,6 +432,7 @@ Some variables support command-line flags: ```bash ./embapi \ --debug \ + --auth-verbose \ --host 0.0.0.0 \ --port 8880 \ --admin-key your-admin-key \ diff --git a/docs/content/getting-started/configuration.md b/docs/content/getting-started/configuration.md index a1d307d..641acd1 100644 --- a/docs/content/getting-started/configuration.md +++ b/docs/content/getting-started/configuration.md @@ -16,6 +16,7 @@ All configuration can be set via environment variables. Use a `.env` file to kee | Variable | Description | Default | Required | |----------|-------------|---------|----------| | `SERVICE_DEBUG` | Enable debug logging | `true` | No | +| `SERVICE_AUTH_VERBOSE` | Enable verbose authentication logging | `false` | No | | `SERVICE_HOST` | Hostname to listen on | `localhost` | No | | `SERVICE_PORT` | Port to listen on | `8880` | No | @@ -43,6 +44,7 @@ Create a `.env` file in the project root: ```bash # Service Configuration SERVICE_DEBUG=false +SERVICE_AUTH_VERBOSE=false SERVICE_HOST=0.0.0.0 SERVICE_PORT=8880 @@ -65,6 +67,7 @@ You can also provide configuration via command-line flags: ```bash ./embapi \ --debug \ + --auth-verbose \ -p 8880 \ --db-host localhost \ --db-port 5432 \ @@ -116,6 +119,7 @@ Configuration is loaded in the following order (later sources override earlier o ```bash # .env (development) SERVICE_DEBUG=true +SERVICE_AUTH_VERBOSE=true SERVICE_HOST=localhost SERVICE_PORT=8880 SERVICE_DBHOST=localhost @@ -132,6 +136,7 @@ ENCRYPTION_KEY=dev-encryption-key-32-chars-min ```bash # .env (production) SERVICE_DEBUG=false +SERVICE_AUTH_VERBOSE=false SERVICE_HOST=0.0.0.0 SERVICE_PORT=8880 SERVICE_DBHOST=prod-db.example.com @@ -143,6 +148,30 @@ SERVICE_ADMINKEY=$(cat /run/secrets/admin_key) ENCRYPTION_KEY=$(cat /run/secrets/encryption_key) ``` +## Logging Configuration + +### Authentication Logging + +The `SERVICE_AUTH_VERBOSE` flag controls authentication log output. This is particularly useful for production environments or during bulk operations: + +- **`SERVICE_AUTH_VERBOSE=true`** (verbose mode): Logs all authentication attempts, including successful and failed authentications. Useful for debugging authentication issues or monitoring security events. + +- **`SERVICE_AUTH_VERBOSE=false`** (quiet mode, default): Suppresses authentication logs. Recommended for production, especially during: + - Bulk upload operations + - High-traffic scenarios + - When crawlers or automated tools access the API frequently + +Example log output with `SERVICE_AUTH_VERBOSE=true`: +``` + Owner authentication successful: alice + Reader auth for owner=bob project=test definition= instance= running... + Checking project access... + Reader authentication successful + Authentication failed. +``` + +With `SERVICE_AUTH_VERBOSE=false`, these messages are suppressed while authentication still works normally. + ## Validation The service validates configuration on startup and will exit with an error if required variables are missing. diff --git a/template.env b/template.env index 88fc454..b9f8b98 100644 --- a/template.env +++ b/template.env @@ -1,6 +1,7 @@ #!/usr/bin/env bash SERVICE_DEBUG=true +SERVICE_AUTH_VERBOSE=false SERVICE_HOST=localhost SERVICE_PORT=8888 SERVICE_DBHOST=localhost From 6b0c5d18d80c89cfe97b9589a516c6d760d0069b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:41:32 +0000 Subject: [PATCH 5/5] Remove short flag -v from AuthVerbose to avoid version flag conflict Co-authored-by: awagner-mainz <2088443+awagner-mainz@users.noreply.github.com> --- docs/content/deployment/environment-variables.md | 2 +- internal/models/options.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/deployment/environment-variables.md b/docs/content/deployment/environment-variables.md index f8c157c..e612284 100644 --- a/docs/content/deployment/environment-variables.md +++ b/docs/content/deployment/environment-variables.md @@ -116,7 +116,7 @@ Enable verbose authentication logging. - **Required:** No - **Default:** `false` - **Environment Variable:** `SERVICE_AUTH_VERBOSE` -- **Command-line Flag:** `-v`, `--auth-verbose` +- **Command-line Flag:** `--auth-verbose` **Description:** Controls authentication log output. When enabled, logs all authentication attempts including successful and failed authentications. When disabled (default), authentication operates silently. diff --git a/internal/models/options.go b/internal/models/options.go index be8bd14..dddfba5 100644 --- a/internal/models/options.go +++ b/internal/models/options.go @@ -3,7 +3,7 @@ package models // Options for the CLI. type Options struct { Debug bool ` env:"SERVICE_DEBUG" doc:"Enable debug logging" short:"d" default:"true"` - AuthVerbose bool `name:"auth-verbose" env:"SERVICE_AUTH_VERBOSE" doc:"Enable verbose authentication logging" short:"v" default:"false"` + AuthVerbose bool `name:"auth-verbose" env:"SERVICE_AUTH_VERBOSE" doc:"Enable verbose authentication logging" default:"false"` Host string ` env:"SERVICE_HOST" doc:"Hostname to listen on" default:"localhost"` Port int ` env:"SERVICE_PORT" doc:"Port to listen on" short:"p" default:"8880"` DBHost string `name:"db-host" env:"SERVICE_DBHOST" doc:"Database hostname" default:"localhost"`