From 17d426258e61f9965e0baf73915372b472c16556 Mon Sep 17 00:00:00 2001 From: Brecci Date: Thu, 26 Feb 2026 16:50:13 -0300 Subject: [PATCH 1/5] test: add first-ever middleware tests for gRPC and HTTP - 17 test functions with 40+ subtests covering both middleware files - gRPC: stripBearer, policyForMethod, grpcErrorFromHTTP, extractTokenFromMD, SubFromMetadata, NewGRPCAuthUnaryPolicy integration - HTTP: checkAuthorization subject construction for normal-user and application tokens, mock server integration, error handling - Documents known bug: application tokens hardcode "admin/" prefix instead of using owner claim from JWT - Add testify v1.11.1 dependency Co-Authored-By: Claude Opus 4.6 --- auth/middleware/middlewareGRPC_test.go | 558 +++++++++++++++++++++++++ auth/middleware/middleware_test.go | 449 ++++++++++++++++++++ go.mod | 57 +-- go.sum | 128 +++--- 4 files changed, 1109 insertions(+), 83 deletions(-) create mode 100644 auth/middleware/middlewareGRPC_test.go create mode 100644 auth/middleware/middleware_test.go diff --git a/auth/middleware/middlewareGRPC_test.go b/auth/middleware/middlewareGRPC_test.go new file mode 100644 index 0000000..ff90515 --- /dev/null +++ b/auth/middleware/middlewareGRPC_test.go @@ -0,0 +1,558 @@ +package middleware + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// --------------------------------------------------------------------------- +// stripBearer +// --------------------------------------------------------------------------- + +func Test_stripBearer(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want string + }{ + { + name: "standard_bearer_prefix", + input: "Bearer token123", + want: "token123", + }, + { + name: "lowercase_bearer_prefix", + input: "bearer token123", + want: "token123", + }, + { + name: "uppercase_bearer_prefix", + input: "BEARER token123", + want: "token123", + }, + { + name: "no_prefix_returns_token_as_is", + input: "token123", + want: "token123", + }, + { + name: "whitespace_around_bearer_and_token", + input: " Bearer token123 ", + want: "token123", + }, + { + name: "empty_string", + input: "", + want: "", + }, + { + // NOTE: "Bearer " is trimmed to "Bearer" (6 chars), which is shorter + // than the 7-char "bearer " prefix check, so stripBearer returns + // the trimmed value as-is. This documents actual behavior. + name: "bearer_prefix_with_no_token_returns_bearer_literal", + input: "Bearer ", + want: "Bearer", + }, + { + // Same trimming behavior as above. + name: "bearer_prefix_only_trailing_spaces_returns_bearer_literal", + input: "Bearer ", + want: "Bearer", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := stripBearer(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} + +// --------------------------------------------------------------------------- +// policyForMethod +// --------------------------------------------------------------------------- + +func Test_policyForMethod(t *testing.T) { + t.Parallel() + + defaultPol := Policy{Resource: "default-res", Action: "default-act"} + specificPol := Policy{Resource: "users", Action: "read"} + + tests := []struct { + name string + cfg PolicyConfig + fullMethod string + wantPolicy Policy + wantFound bool + }{ + { + name: "method_found_in_method_policies", + cfg: PolicyConfig{ + MethodPolicies: map[string]Policy{ + "/pkg.Service/GetUser": specificPol, + }, + }, + fullMethod: "/pkg.Service/GetUser", + wantPolicy: specificPol, + wantFound: true, + }, + { + name: "method_not_found_falls_back_to_default_policy", + cfg: PolicyConfig{ + MethodPolicies: map[string]Policy{ + "/pkg.Service/GetUser": specificPol, + }, + DefaultPolicy: &defaultPol, + }, + fullMethod: "/pkg.Service/DeleteUser", + wantPolicy: defaultPol, + wantFound: true, + }, + { + name: "method_not_found_no_default_returns_false", + cfg: PolicyConfig{ + MethodPolicies: map[string]Policy{ + "/pkg.Service/GetUser": specificPol, + }, + }, + fullMethod: "/pkg.Service/DeleteUser", + wantPolicy: Policy{}, + wantFound: false, + }, + { + name: "nil_method_policies_with_default_returns_default", + cfg: PolicyConfig{ + MethodPolicies: nil, + DefaultPolicy: &defaultPol, + }, + fullMethod: "/pkg.Service/AnyMethod", + wantPolicy: defaultPol, + wantFound: true, + }, + { + name: "nil_method_policies_no_default_returns_false", + cfg: PolicyConfig{ + MethodPolicies: nil, + DefaultPolicy: nil, + }, + fullMethod: "/pkg.Service/AnyMethod", + wantPolicy: Policy{}, + wantFound: false, + }, + { + name: "empty_method_policies_with_default", + cfg: PolicyConfig{ + MethodPolicies: map[string]Policy{}, + DefaultPolicy: &defaultPol, + }, + fullMethod: "/pkg.Service/AnyMethod", + wantPolicy: defaultPol, + wantFound: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gotPolicy, gotFound := policyForMethod(tt.cfg, tt.fullMethod) + assert.Equal(t, tt.wantFound, gotFound) + assert.Equal(t, tt.wantPolicy, gotPolicy) + }) + } +} + +// --------------------------------------------------------------------------- +// grpcErrorFromHTTP +// --------------------------------------------------------------------------- + +func Test_grpcErrorFromHTTP(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + httpStatus int + wantCode codes.Code + wantMsg string + }{ + { + name: "401_maps_to_unauthenticated", + httpStatus: http.StatusUnauthorized, + wantCode: codes.Unauthenticated, + wantMsg: "unauthenticated", + }, + { + name: "403_maps_to_permission_denied", + httpStatus: http.StatusForbidden, + wantCode: codes.PermissionDenied, + wantMsg: "forbidden", + }, + { + name: "500_maps_to_internal", + httpStatus: http.StatusInternalServerError, + wantCode: codes.Internal, + wantMsg: "internal error", + }, + { + name: "0_default_maps_to_internal", + httpStatus: 0, + wantCode: codes.Internal, + wantMsg: "internal error", + }, + { + name: "404_unmapped_maps_to_internal", + httpStatus: http.StatusNotFound, + wantCode: codes.Internal, + wantMsg: "internal error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := grpcErrorFromHTTP(tt.httpStatus) + require.Error(t, err) + + st, ok := status.FromError(err) + require.True(t, ok, "expected a gRPC status error") + assert.Equal(t, tt.wantCode, st.Code()) + assert.Equal(t, tt.wantMsg, st.Message()) + }) + } +} + +// --------------------------------------------------------------------------- +// extractTokenFromMD +// --------------------------------------------------------------------------- + +func Test_extractTokenFromMD(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + ctx context.Context + wantToken string + wantOK bool + }{ + { + name: "valid_bearer_token_in_metadata", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("authorization", "Bearer token123"), + ), + wantToken: "token123", + wantOK: true, + }, + { + name: "no_metadata_in_context", + ctx: context.Background(), + wantToken: "", + wantOK: false, + }, + { + name: "empty_authorization_value", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("authorization", ""), + ), + wantToken: "", + wantOK: false, + }, + { + // NOTE: "Bearer " trimmed to "Bearer" (6 chars) which is below the + // 7-char prefix check threshold. stripBearer returns "Bearer" as a + // literal token and extractTokenFromMD treats it as non-empty. + name: "authorization_with_bearer_prefix_only_returns_bearer_literal", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("authorization", "Bearer "), + ), + wantToken: "Bearer", + wantOK: true, + }, + { + name: "multiple_authorization_values_takes_first", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs( + "authorization", "Bearer first-token", + "authorization", "Bearer second-token", + ), + ), + wantToken: "first-token", + wantOK: true, + }, + { + name: "token_without_bearer_prefix", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("authorization", "raw-token-value"), + ), + wantToken: "raw-token-value", + wantOK: true, + }, + { + name: "metadata_present_but_no_authorization_key", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("content-type", "application/json"), + ), + wantToken: "", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gotToken, gotOK := extractTokenFromMD(tt.ctx) + assert.Equal(t, tt.wantOK, gotOK) + assert.Equal(t, tt.wantToken, gotToken) + }) + } +} + +// --------------------------------------------------------------------------- +// SubFromMetadata +// --------------------------------------------------------------------------- + +func TestSubFromMetadata(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + key string + ctx context.Context + wantSub string + wantErr bool + }{ + { + name: "key_present_in_metadata", + key: "x-tenant-id", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("x-tenant-id", "tenant-abc"), + ), + wantSub: "tenant-abc", + wantErr: false, + }, + { + name: "key_absent_in_metadata", + key: "x-tenant-id", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("other-key", "value"), + ), + wantSub: "", + wantErr: false, + }, + { + name: "no_metadata_in_context", + key: "x-tenant-id", + ctx: context.Background(), + wantSub: "", + wantErr: false, + }, + { + name: "case_insensitive_key_lookup", + key: "X-Tenant-ID", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("x-tenant-id", "tenant-xyz"), + ), + wantSub: "tenant-xyz", + wantErr: false, + }, + { + name: "key_with_leading_trailing_whitespace", + key: " x-tenant-id ", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("x-tenant-id", "tenant-trimmed"), + ), + wantSub: "tenant-trimmed", + wantErr: false, + }, + { + name: "multiple_values_returns_first", + key: "x-scope", + ctx: metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("x-scope", "first", "x-scope", "second"), + ), + wantSub: "first", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + resolver := SubFromMetadata(tt.key) + gotSub, err := resolver(tt.ctx, "/unused.Method", nil) + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.wantSub, gotSub) + }) + } +} + +// --------------------------------------------------------------------------- +// NewGRPCAuthUnaryPolicy (integration-level) +// --------------------------------------------------------------------------- + +func TestNewGRPCAuthUnaryPolicy(t *testing.T) { + t.Parallel() + + handlerCalled := false + + noopHandler := func(_ context.Context, _ any) (any, error) { + handlerCalled = true + return "ok", nil + } + + dummyInfo := &grpc.UnaryServerInfo{ + FullMethod: "/pkg.Service/DoThing", + } + + t.Run("auth_disabled_passes_through", func(t *testing.T) { + t.Parallel() + + called := false + handler := func(_ context.Context, _ any) (any, error) { + called = true + return "ok", nil + } + + auth := &AuthClient{Address: "http://localhost:9999", Enabled: false} + interceptor := NewGRPCAuthUnaryPolicy(auth, PolicyConfig{}) + + resp, err := interceptor(context.Background(), "req", dummyInfo, handler) + require.NoError(t, err) + assert.Equal(t, "ok", resp) + assert.True(t, called) + }) + + t.Run("auth_nil_passes_through", func(t *testing.T) { + t.Parallel() + + called := false + handler := func(_ context.Context, _ any) (any, error) { + called = true + return "ok", nil + } + + interceptor := NewGRPCAuthUnaryPolicy(nil, PolicyConfig{}) + + resp, err := interceptor(context.Background(), "req", dummyInfo, handler) + require.NoError(t, err) + assert.Equal(t, "ok", resp) + assert.True(t, called) + }) + + t.Run("auth_enabled_but_empty_address_passes_through", func(t *testing.T) { + t.Parallel() + + called := false + handler := func(_ context.Context, _ any) (any, error) { + called = true + return "ok", nil + } + + auth := &AuthClient{Address: "", Enabled: true} + interceptor := NewGRPCAuthUnaryPolicy(auth, PolicyConfig{}) + + resp, err := interceptor(context.Background(), "req", dummyInfo, handler) + require.NoError(t, err) + assert.Equal(t, "ok", resp) + assert.True(t, called) + }) + + t.Run("missing_token_returns_unauthenticated", func(t *testing.T) { + t.Parallel() + + auth := &AuthClient{Address: "http://localhost:9999", Enabled: true} + interceptor := NewGRPCAuthUnaryPolicy(auth, PolicyConfig{}) + + // Context without any metadata -> no token + resp, err := interceptor(context.Background(), "req", dummyInfo, noopHandler) + require.Error(t, err) + assert.Nil(t, resp) + + st, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, codes.Unauthenticated, st.Code()) + assert.Contains(t, st.Message(), "missing token") + }) + + t.Run("bearer_prefix_only_passes_token_check_but_fails_policy_lookup", func(t *testing.T) { + t.Parallel() + + // NOTE: "Bearer " is trimmed to "Bearer" (6 chars) by stripBearer, + // which is treated as a non-empty token. The interceptor then proceeds + // to the policy lookup phase, which fails because no policy is + // configured for the method. + auth := &AuthClient{Address: "http://localhost:9999", Enabled: true} + interceptor := NewGRPCAuthUnaryPolicy(auth, PolicyConfig{}) + + ctx := metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("authorization", "Bearer "), + ) + + resp, err := interceptor(ctx, "req", dummyInfo, noopHandler) + require.Error(t, err) + assert.Nil(t, resp) + + st, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, codes.Internal, st.Code()) + assert.Contains(t, st.Message(), "internal configuration error") + }) + + t.Run("no_policy_for_method_and_no_default_returns_internal", func(t *testing.T) { + t.Parallel() + + auth := &AuthClient{Address: "http://localhost:9999", Enabled: true} + cfg := PolicyConfig{ + MethodPolicies: map[string]Policy{ + "/pkg.Service/OtherMethod": {Resource: "other", Action: "read"}, + }, + // No DefaultPolicy + } + interceptor := NewGRPCAuthUnaryPolicy(auth, cfg) + + ctx := metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("authorization", "Bearer valid-token"), + ) + + resp, err := interceptor(ctx, "req", dummyInfo, noopHandler) + require.Error(t, err) + assert.Nil(t, resp) + + st, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, codes.Internal, st.Code()) + assert.Contains(t, st.Message(), "internal configuration error") + }) + + // Prevent compiler from optimizing away the handlerCalled variable + _ = handlerCalled +} diff --git a/auth/middleware/middleware_test.go b/auth/middleware/middleware_test.go new file mode 100644 index 0000000..460310b --- /dev/null +++ b/auth/middleware/middleware_test.go @@ -0,0 +1,449 @@ +package middleware + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/LerianStudio/lib-commons/v2/commons/log" + jwt "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +// createTestJWT builds a signed JWT string for testing. +// checkAuthorization uses ParseUnverified so the signing key does not matter. +func createTestJWT(claims jwt.MapClaims) string { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + signed, err := token.SignedString([]byte("test-secret")) + if err != nil { + // This should never happen in tests with a valid key. + panic("failed to sign test JWT: " + err.Error()) + } + + return signed +} + +// mockAuthServer returns an httptest.Server that responds to POST /v1/authorize. +func mockAuthServer(t *testing.T, authorized bool, statusCode int) *httptest.Server { + t.Helper() + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + + resp := AuthResponse{Authorized: authorized} + + err := json.NewEncoder(w).Encode(resp) + if err != nil { + t.Errorf("mock server: failed to encode response: %v", err) + } + })) +} + +// testLogger is a minimal log.Logger implementation for tests that discards all output. +type testLogger struct{} + +func (l *testLogger) Info(_ ...any) {} +func (l *testLogger) Infof(_ string, _ ...any) {} +func (l *testLogger) Infoln(_ ...any) {} +func (l *testLogger) Error(_ ...any) {} +func (l *testLogger) Errorf(_ string, _ ...any) {} +func (l *testLogger) Errorln(_ ...any) {} +func (l *testLogger) Warn(_ ...any) {} +func (l *testLogger) Warnf(_ string, _ ...any) {} +func (l *testLogger) Warnln(_ ...any) {} +func (l *testLogger) Debug(_ ...any) {} +func (l *testLogger) Debugf(_ string, _ ...any) {} +func (l *testLogger) Debugln(_ ...any) {} +func (l *testLogger) Fatal(_ ...any) {} +func (l *testLogger) Fatalf(_ string, _ ...any) {} +func (l *testLogger) Fatalln(_ ...any) {} +func (l *testLogger) WithFields(_ ...any) log.Logger { return l } +func (l *testLogger) WithDefaultMessageTemplate(_ string) log.Logger { return l } +func (l *testLogger) Sync() error { return nil } + +// --------------------------------------------------------------------------- +// checkAuthorization - subject construction +// --------------------------------------------------------------------------- + +func TestCheckAuthorization_NormalUser_SubjectConstruction(t *testing.T) { + t.Parallel() + + // Mock server captures the request body to verify the constructed subject. + var capturedBody map[string]string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(&capturedBody) + if err != nil { + t.Errorf("mock server: failed to decode request body: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + resp := AuthResponse{Authorized: true} + + encErr := json.NewEncoder(w).Encode(resp) + if encErr != nil { + t.Errorf("mock server: failed to encode response: %v", encErr) + } + })) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + token := createTestJWT(jwt.MapClaims{ + "type": "normal-user", + "owner": "acme-org", + "sub": "user123", + }) + + authorized, statusCode, err := auth.checkAuthorization( + context.Background(), "initial-sub", "resource", "action", token, + ) + + require.NoError(t, err) + assert.True(t, authorized) + assert.Equal(t, http.StatusOK, statusCode) + + // For normal-user, sub should be "owner/sub-from-jwt" (overrides the initial sub parameter). + assert.Equal(t, "acme-org/user123", capturedBody["sub"]) +} + +func TestCheckAuthorization_ApplicationUser_SubjectConstruction(t *testing.T) { + t.Parallel() + + // Documents the current behavior: non-normal-user types get "admin/-editor-role". + var capturedBody map[string]string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(&capturedBody) + if err != nil { + t.Errorf("mock server: failed to decode request body: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + resp := AuthResponse{Authorized: true} + + encErr := json.NewEncoder(w).Encode(resp) + if encErr != nil { + t.Errorf("mock server: failed to encode response: %v", encErr) + } + })) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + token := createTestJWT(jwt.MapClaims{ + "type": "application", + "name": "my-app", + "sub": "app-sub", + }) + + authorized, statusCode, err := auth.checkAuthorization( + context.Background(), "my-app", "resource", "action", token, + ) + + require.NoError(t, err) + assert.True(t, authorized) + assert.Equal(t, http.StatusOK, statusCode) + + // BUG: hardcodes "admin/" prefix. The sub parameter is used as-is with the + // "admin/-editor-role" pattern, regardless of the actual user type. + assert.Equal(t, "admin/my-app-editor-role", capturedBody["sub"]) +} + +func TestCheckAuthorization_MissingOwnerClaim(t *testing.T) { + t.Parallel() + + server := mockAuthServer(t, true, http.StatusOK) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + // normal-user without "owner" claim should cause an error. + token := createTestJWT(jwt.MapClaims{ + "type": "normal-user", + "sub": "user123", + // "owner" is intentionally missing + }) + + authorized, statusCode, err := auth.checkAuthorization( + context.Background(), "sub", "resource", "action", token, + ) + + require.Error(t, err) + assert.False(t, authorized) + assert.Equal(t, http.StatusUnauthorized, statusCode) + assert.Contains(t, err.Error(), "missing owner claim") +} + +func TestCheckAuthorization_MockServerReturnsAuthorizedTrue(t *testing.T) { + t.Parallel() + + server := mockAuthServer(t, true, http.StatusOK) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + token := createTestJWT(jwt.MapClaims{ + "type": "normal-user", + "owner": "org1", + "sub": "user1", + }) + + authorized, statusCode, err := auth.checkAuthorization( + context.Background(), "sub", "resource", "read", token, + ) + + require.NoError(t, err) + assert.True(t, authorized) + assert.Equal(t, http.StatusOK, statusCode) +} + +func TestCheckAuthorization_MockServerReturnsAuthorizedFalse(t *testing.T) { + t.Parallel() + + server := mockAuthServer(t, false, http.StatusOK) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + token := createTestJWT(jwt.MapClaims{ + "type": "normal-user", + "owner": "org1", + "sub": "user1", + }) + + authorized, statusCode, err := auth.checkAuthorization( + context.Background(), "sub", "resource", "read", token, + ) + + require.NoError(t, err) + assert.False(t, authorized) + assert.Equal(t, http.StatusOK, statusCode) +} + +func TestCheckAuthorization_MockServerReturnsForbiddenWithErrorBody(t *testing.T) { + t.Parallel() + + // When the auth server returns a non-200 response with a Response body that + // has a non-empty Code field, checkAuthorization returns an error. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + + resp := map[string]string{ + "code": "FORBIDDEN", + "title": "Forbidden", + "message": "You do not have permission", + } + + err := json.NewEncoder(w).Encode(resp) + if err != nil { + t.Errorf("mock server: failed to encode response: %v", err) + } + })) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + token := createTestJWT(jwt.MapClaims{ + "type": "normal-user", + "owner": "org1", + "sub": "user1", + }) + + authorized, statusCode, err := auth.checkAuthorization( + context.Background(), "sub", "resource", "write", token, + ) + + require.Error(t, err) + assert.False(t, authorized) + assert.Equal(t, http.StatusForbidden, statusCode) +} + +func TestCheckAuthorization_InvalidToken(t *testing.T) { + t.Parallel() + + server := mockAuthServer(t, true, http.StatusOK) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + // Completely invalid JWT string that cannot be parsed. + invalidToken := "not-a-valid-jwt" + + authorized, statusCode, err := auth.checkAuthorization( + context.Background(), "sub", "resource", "action", invalidToken, + ) + + require.Error(t, err) + assert.False(t, authorized) + assert.Equal(t, http.StatusInternalServerError, statusCode) +} + +func TestCheckAuthorization_EmptyTypeClaim_TreatedAsNonNormalUser(t *testing.T) { + t.Parallel() + + // When the "type" claim is empty or absent, userType != normalUser, + // so the code takes the admin/ branch. + var capturedBody map[string]string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(&capturedBody) + if err != nil { + t.Errorf("mock server: failed to decode request body: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + resp := AuthResponse{Authorized: true} + + encErr := json.NewEncoder(w).Encode(resp) + if encErr != nil { + t.Errorf("mock server: failed to encode response: %v", encErr) + } + })) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + // No "type" claim at all -> defaults to empty string -> non-normal-user path + token := createTestJWT(jwt.MapClaims{ + "sub": "some-app", + }) + + authorized, statusCode, err := auth.checkAuthorization( + context.Background(), "some-app", "resource", "action", token, + ) + + require.NoError(t, err) + assert.True(t, authorized) + assert.Equal(t, http.StatusOK, statusCode) + assert.Equal(t, "admin/some-app-editor-role", capturedBody["sub"]) +} + +func TestCheckAuthorization_MockServerDown(t *testing.T) { + t.Parallel() + + // Use a server and immediately close it to simulate a connection failure. + server := mockAuthServer(t, true, http.StatusOK) + server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + token := createTestJWT(jwt.MapClaims{ + "type": "normal-user", + "owner": "org1", + "sub": "user1", + }) + + authorized, statusCode, err := auth.checkAuthorization( + context.Background(), "sub", "resource", "read", token, + ) + + require.Error(t, err) + assert.False(t, authorized) + assert.Equal(t, http.StatusInternalServerError, statusCode) + assert.Contains(t, err.Error(), "failed to make request") +} + +func TestCheckAuthorization_ServerReturnsInvalidJSON(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // Write invalid JSON + _, _ = w.Write([]byte("not-json")) + })) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + token := createTestJWT(jwt.MapClaims{ + "type": "normal-user", + "owner": "org1", + "sub": "user1", + }) + + authorized, statusCode, err := auth.checkAuthorization( + context.Background(), "sub", "resource", "read", token, + ) + + require.Error(t, err) + assert.False(t, authorized) + assert.Equal(t, http.StatusInternalServerError, statusCode) + assert.Contains(t, err.Error(), "failed to unmarshal") +} + +// --------------------------------------------------------------------------- +// AuthResponse JSON serialization +// --------------------------------------------------------------------------- + +func TestAuthResponse_JSONRoundTrip(t *testing.T) { + t.Parallel() + + original := AuthResponse{Authorized: true} + + data, err := json.Marshal(original) + require.NoError(t, err) + + var decoded AuthResponse + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + assert.Equal(t, original.Authorized, decoded.Authorized) +} diff --git a/go.mod b/go.mod index 069e9c0..56ad4d5 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/LerianStudio/lib-auth/v2 -go 1.23.2 - -toolchain go1.23.3 +go 1.24.0 require ( github.com/LerianStudio/lib-commons/v2 v2.7.0-beta.3 github.com/gofiber/fiber/v2 v2.52.9 github.com/golang-jwt/jwt/v5 v5.3.0 - go.opentelemetry.io/otel v1.37.0 + github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/otel v1.38.0 + google.golang.org/grpc v1.76.0 ) require github.com/google/uuid v1.6.0 // indirect @@ -17,10 +17,11 @@ require ( github.com/Masterminds/squirrel v1.5.4 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect @@ -29,34 +30,36 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/sony/gobreaker v1.0.0 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.64.0 // indirect + github.com/valyala/fasthttp v1.67.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/bridges/otelzap v0.12.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect - go.opentelemetry.io/otel/log v0.13.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/sdk v1.37.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/log v0.14.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect - go.uber.org/mock v0.5.2 // indirect + go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect - google.golang.org/grpc v1.74.2 // indirect - google.golang.org/protobuf v1.36.6 // indirect + go.uber.org/zap v1.27.1 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d71cef3..efdd84e 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,12 @@ -github.com/LerianStudio/lib-commons/v2 v2.2.0 h1:pAyerAucWl42gvVUdGpBdUDRBUvNAk7OhtGZxyJ25aE= -github.com/LerianStudio/lib-commons/v2 v2.2.0/go.mod h1:6esN/Ao/Xkp/QwvYt8wXQNMgrhb3exOstKUIPuSUBDM= +github.com/LerianStudio/lib-commons/v2 v2.7.0-beta.3 h1:13Y37Ed0xDd4UMks3Ag6nbBzakz6vxu2XLMkPUPVo1M= +github.com/LerianStudio/lib-commons/v2 v2.7.0-beta.3/go.mod h1:p3gWgBPt9NSKMmRstYtlWOV1BkcSe6yFpdb1i+/NdrQ= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -27,12 +28,16 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= @@ -51,79 +56,90 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= +github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og= -github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA= +github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac= +github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/bridges/otelzap v0.12.0 h1:FGre0nZh5BSw7G73VpT3xs38HchsfPsa2aZtMp0NPOs= -go.opentelemetry.io/contrib/bridges/otelzap v0.12.0/go.mod h1:X2PYPViI2wTPIMIOBjG17KNybTzsrATnvPJ02kkz7LM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 h1:z6lNIajgEBVtQZHjfw2hAccPEBDs+nx58VemmXWa2ec= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0/go.mod h1:+kyc3bRx/Qkq05P6OCu3mTEIOxYRYzoIg+JsUp5X+PM= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 h1:zG8GlgXCJQd5BU98C0hZnBbElszTmUgCNCfYneaDL0A= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= -go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls= -go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E= -go.opentelemetry.io/otel/log/logtest v0.13.0 h1:xxaIcgoEEtnwdgj6D6Uo9K/Dynz9jqIxSDu2YObJ69Q= -go.opentelemetry.io/otel/log/logtest v0.13.0/go.mod h1:+OrkmsAH38b+ygyag1tLjSFMYiES5UHggzrtY1IIEA8= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ= -go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw= -go.opentelemetry.io/otel/sdk/log/logtest v0.13.0 h1:9yio6AFZ3QD9j9oqshV1Ibm9gPLlHNxurno5BreMtIA= -go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 h1:aBKdhLVieqvwWe9A79UHI/0vgp2t/s2euY8X59pGRlw= +go.opentelemetry.io/contrib/bridges/otelzap v0.13.0/go.mod h1:SYqtxLQE7iINgh6WFuVi2AI70148B8EI35DSk0Wr8m4= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/log/logtest v0.14.0 h1:BGTqNeluJDK2uIHAY8lRqxjVAYfqgcaTbVk1n3MWe5A= +go.opentelemetry.io/otel/log/logtest v0.14.0/go.mod h1:IuguGt8XVP4XA4d2oEEDMVDBBCesMg8/tSGWDjuKfoA= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= +go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= -google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From f4dae96c438554ca65c8fff5336c45410cb9d67d Mon Sep 17 00:00:00 2001 From: Brecci Date: Thu, 26 Feb 2026 16:55:23 -0300 Subject: [PATCH 2/5] feat: make gRPC interceptor tenant-aware with streaming support - Add extractTenantClaims to parse tenantId/tenantSlug/owner from JWT - Propagate tenant metadata (md-tenant-id, md-tenant-slug, md-tenant-owner) in unary interceptor when MULTI_TENANT_ENABLED=true - Add NewGRPCAuthStreamPolicy streaming interceptor mirroring unary behavior - Add wrappedServerStream to override context for streaming calls - Add tests for extraction, propagation, and streaming interceptor Co-Authored-By: Claude Opus 4.6 --- auth/middleware/middlewareGRPC.go | 125 +++++++++ auth/middleware/middlewareGRPC_test.go | 369 +++++++++++++++++++++++++ 2 files changed, 494 insertions(+) diff --git a/auth/middleware/middlewareGRPC.go b/auth/middleware/middlewareGRPC.go index bd0b91d..43f612a 100644 --- a/auth/middleware/middlewareGRPC.go +++ b/auth/middleware/middlewareGRPC.go @@ -2,12 +2,15 @@ package middleware import ( "context" + "errors" "fmt" "net/http" + "os" "strings" "github.com/LerianStudio/lib-commons/v2/commons" "github.com/LerianStudio/lib-commons/v2/commons/opentelemetry" + jwt "github.com/golang-jwt/jwt/v5" "go.opentelemetry.io/otel/attribute" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -97,6 +100,27 @@ func NewGRPCAuthUnaryPolicy(auth *AuthClient, cfg PolicyConfig) grpc.UnaryServer return nil, status.Error(codes.PermissionDenied, "forbidden") } + // Propagate tenant claims if multi-tenant mode is enabled + if os.Getenv("MULTI_TENANT_ENABLED") == "true" { + tenantID, tenantSlug, tOwner, _ := extractTenantClaims(token) + md, _ := metadata.FromIncomingContext(ctx) + md = md.Copy() + + if tenantID != "" { + md.Set("md-tenant-id", tenantID) + } + + if tenantSlug != "" { + md.Set("md-tenant-slug", tenantSlug) + } + + if tOwner != "" { + md.Set("md-tenant-owner", tOwner) + } + + ctx = metadata.NewIncomingContext(ctx, md) + } + return handler(ctx, req) } } @@ -179,3 +203,104 @@ func SubFromMetadata(key string) func(ctx context.Context, fullMethod string, re return vals[0], nil } } + +// extractTenantClaims extracts tenant-related claims from a JWT without signature verification. +// Returns tenantID, tenantSlug, and owner from the token's custom claims. +// Used by gRPC interceptors to propagate tenant context to downstream services. +func extractTenantClaims(tokenString string) (tenantID, tenantSlug, owner string, err error) { + token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{}) + if err != nil { + return "", "", "", err + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return "", "", "", errors.New("invalid token claims") + } + + tenantID, _ = claims["tenantId"].(string) + tenantSlug, _ = claims["tenantSlug"].(string) + owner, _ = claims["owner"].(string) + + return tenantID, tenantSlug, owner, nil +} + +// NewGRPCAuthStreamPolicy authorizes streaming RPCs via per-method Policy. +// Mirrors NewGRPCAuthUnaryPolicy behavior for streaming calls: +// - Resolves Policy by info.FullMethod; falls back to DefaultPolicy. +// - Rejects missing tokens with codes.Unauthenticated. +// - Propagates tenant claims when MULTI_TENANT_ENABLED=true. +func NewGRPCAuthStreamPolicy(auth *AuthClient, cfg PolicyConfig) grpc.StreamServerInterceptor { + return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + if auth == nil || !auth.Enabled || auth.Address == "" { + return handler(srv, ss) + } + + ctx := ss.Context() + token, ok := extractTokenFromMD(ctx) + + if !ok || commons.IsNilOrEmpty(&token) { + return status.Error(codes.Unauthenticated, "missing token") + } + + pol, found := policyForMethod(cfg, info.FullMethod) + if !found { + return status.Error(codes.Internal, "internal configuration error") + } + + var sub string + + if cfg.SubResolver != nil { + var err error + + sub, err = cfg.SubResolver(ctx, info.FullMethod, nil) + if err != nil { + return status.Error(codes.Internal, "internal configuration error") + } + } + + authorized, httpStatus, err := auth.checkAuthorization(ctx, sub, pol.Resource, pol.Action, token) + if err != nil { + return grpcErrorFromHTTP(httpStatus) + } + + if !authorized { + return status.Error(codes.PermissionDenied, "forbidden") + } + + // Propagate tenant claims if multi-tenant mode is enabled + if os.Getenv("MULTI_TENANT_ENABLED") == "true" { + tenantID, tenantSlug, tOwner, _ := extractTenantClaims(token) + md, _ := metadata.FromIncomingContext(ctx) + md = md.Copy() + + if tenantID != "" { + md.Set("md-tenant-id", tenantID) + } + + if tenantSlug != "" { + md.Set("md-tenant-slug", tenantSlug) + } + + if tOwner != "" { + md.Set("md-tenant-owner", tOwner) + } + + ctx = metadata.NewIncomingContext(ctx, md) + ss = &wrappedServerStream{ServerStream: ss, ctx: ctx} + } + + return handler(srv, ss) + } +} + +// wrappedServerStream wraps grpc.ServerStream to override Context(). +type wrappedServerStream struct { + grpc.ServerStream + ctx context.Context +} + +// Context returns the wrapped context. +func (w *wrappedServerStream) Context() context.Context { + return w.ctx +} diff --git a/auth/middleware/middlewareGRPC_test.go b/auth/middleware/middlewareGRPC_test.go index ff90515..e092375 100644 --- a/auth/middleware/middlewareGRPC_test.go +++ b/auth/middleware/middlewareGRPC_test.go @@ -5,6 +5,7 @@ import ( "net/http" "testing" + jwt "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" @@ -556,3 +557,371 @@ func TestNewGRPCAuthUnaryPolicy(t *testing.T) { // Prevent compiler from optimizing away the handlerCalled variable _ = handlerCalled } + +// --------------------------------------------------------------------------- +// extractTenantClaims +// --------------------------------------------------------------------------- + +func Test_extractTenantClaims(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + tokenString string + wantTenantID string + wantTenantSlug string + wantOwner string + wantErr bool + }{ + { + name: "valid_jwt_with_all_tenant_claims", + tokenString: createTestJWT(jwt.MapClaims{ + "tenantId": "tid-123", + "tenantSlug": "acme-corp", + "owner": "owner-456", + }), + wantTenantID: "tid-123", + wantTenantSlug: "acme-corp", + wantOwner: "owner-456", + wantErr: false, + }, + { + name: "jwt_with_only_owner", + tokenString: createTestJWT(jwt.MapClaims{ + "owner": "owner-only", + }), + wantTenantID: "", + wantTenantSlug: "", + wantOwner: "owner-only", + wantErr: false, + }, + { + name: "jwt_with_only_tenantId", + tokenString: createTestJWT(jwt.MapClaims{ + "tenantId": "tid-only", + }), + wantTenantID: "tid-only", + wantTenantSlug: "", + wantOwner: "", + wantErr: false, + }, + { + name: "invalid_token", + tokenString: "not.a.valid.jwt", + wantTenantID: "", + wantTenantSlug: "", + wantOwner: "", + wantErr: true, + }, + { + name: "empty_token", + tokenString: "", + wantTenantID: "", + wantTenantSlug: "", + wantOwner: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + tenantID, tenantSlug, owner, err := extractTenantClaims(tt.tokenString) + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.wantTenantID, tenantID) + assert.Equal(t, tt.wantTenantSlug, tenantSlug) + assert.Equal(t, tt.wantOwner, owner) + }) + } +} + +// --------------------------------------------------------------------------- +// NewGRPCAuthUnaryPolicy - tenant propagation +// --------------------------------------------------------------------------- + +func TestNewGRPCAuthUnaryPolicy_TenantPropagation(t *testing.T) { + // Cannot use t.Parallel() because subtests use t.Setenv which modifies process env. + + t.Run("multi_tenant_enabled_propagates_tenant_metadata", func(t *testing.T) { + t.Setenv("MULTI_TENANT_ENABLED", "true") + + server := mockAuthServer(t, true, http.StatusOK) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + token := createTestJWT(jwt.MapClaims{ + "type": "normal-user", + "owner": "org-owner", + "sub": "user1", + "tenantId": "tid-100", + "tenantSlug": "acme", + }) + + defaultPol := Policy{Resource: "res", Action: "read"} + cfg := PolicyConfig{DefaultPolicy: &defaultPol} + interceptor := NewGRPCAuthUnaryPolicy(auth, cfg) + + ctx := metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("authorization", "Bearer "+token), + ) + + var capturedCtx context.Context + + handler := func(ctx context.Context, _ any) (any, error) { + capturedCtx = ctx + return "ok", nil + } + + info := &grpc.UnaryServerInfo{FullMethod: "/pkg.Service/DoThing"} + + resp, err := interceptor(ctx, "req", info, handler) + require.NoError(t, err) + assert.Equal(t, "ok", resp) + + // Verify tenant metadata was propagated + md, ok := metadata.FromIncomingContext(capturedCtx) + require.True(t, ok) + assert.Equal(t, []string{"tid-100"}, md.Get("md-tenant-id")) + assert.Equal(t, []string{"acme"}, md.Get("md-tenant-slug")) + assert.Equal(t, []string{"org-owner"}, md.Get("md-tenant-owner")) + }) + + t.Run("multi_tenant_disabled_no_tenant_metadata", func(t *testing.T) { + t.Setenv("MULTI_TENANT_ENABLED", "false") + + server := mockAuthServer(t, true, http.StatusOK) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + token := createTestJWT(jwt.MapClaims{ + "type": "normal-user", + "owner": "org-owner", + "sub": "user1", + "tenantId": "tid-100", + "tenantSlug": "acme", + }) + + defaultPol := Policy{Resource: "res", Action: "read"} + cfg := PolicyConfig{DefaultPolicy: &defaultPol} + interceptor := NewGRPCAuthUnaryPolicy(auth, cfg) + + ctx := metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("authorization", "Bearer "+token), + ) + + var capturedCtx context.Context + + handler := func(ctx context.Context, _ any) (any, error) { + capturedCtx = ctx + return "ok", nil + } + + info := &grpc.UnaryServerInfo{FullMethod: "/pkg.Service/DoThing"} + + resp, err := interceptor(ctx, "req", info, handler) + require.NoError(t, err) + assert.Equal(t, "ok", resp) + + // Verify no tenant metadata was added + md, ok := metadata.FromIncomingContext(capturedCtx) + require.True(t, ok) + assert.Empty(t, md.Get("md-tenant-id")) + assert.Empty(t, md.Get("md-tenant-slug")) + assert.Empty(t, md.Get("md-tenant-owner")) + }) +} + +// --------------------------------------------------------------------------- +// NewGRPCAuthStreamPolicy +// --------------------------------------------------------------------------- + +// fakeServerStream is a minimal grpc.ServerStream for testing. +type fakeServerStream struct { + grpc.ServerStream + ctx context.Context +} + +func (f *fakeServerStream) Context() context.Context { + return f.ctx +} + +func TestNewGRPCAuthStreamPolicy(t *testing.T) { + // Cannot use t.Parallel() because a subtest uses t.Setenv which modifies process env. + + dummyInfo := &grpc.StreamServerInfo{ + FullMethod: "/pkg.Service/StreamThing", + } + + t.Run("auth_disabled_passes_through", func(t *testing.T) { + t.Parallel() + + called := false + + handler := func(_ any, _ grpc.ServerStream) error { + called = true + return nil + } + + auth := &AuthClient{Address: "http://localhost:9999", Enabled: false} + interceptor := NewGRPCAuthStreamPolicy(auth, PolicyConfig{}) + + ss := &fakeServerStream{ctx: context.Background()} + + err := interceptor(nil, ss, dummyInfo, handler) + require.NoError(t, err) + assert.True(t, called) + }) + + t.Run("auth_nil_passes_through", func(t *testing.T) { + t.Parallel() + + called := false + + handler := func(_ any, _ grpc.ServerStream) error { + called = true + return nil + } + + interceptor := NewGRPCAuthStreamPolicy(nil, PolicyConfig{}) + + ss := &fakeServerStream{ctx: context.Background()} + + err := interceptor(nil, ss, dummyInfo, handler) + require.NoError(t, err) + assert.True(t, called) + }) + + t.Run("missing_token_returns_unauthenticated", func(t *testing.T) { + t.Parallel() + + handler := func(_ any, _ grpc.ServerStream) error { + return nil + } + + auth := &AuthClient{Address: "http://localhost:9999", Enabled: true} + interceptor := NewGRPCAuthStreamPolicy(auth, PolicyConfig{}) + + // Context without any metadata -> no token + ss := &fakeServerStream{ctx: context.Background()} + + err := interceptor(nil, ss, dummyInfo, handler) + require.Error(t, err) + + st, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, codes.Unauthenticated, st.Code()) + assert.Contains(t, st.Message(), "missing token") + }) + + t.Run("no_policy_for_method_returns_internal", func(t *testing.T) { + t.Parallel() + + handler := func(_ any, _ grpc.ServerStream) error { + return nil + } + + auth := &AuthClient{Address: "http://localhost:9999", Enabled: true} + cfg := PolicyConfig{ + MethodPolicies: map[string]Policy{ + "/pkg.Service/OtherMethod": {Resource: "other", Action: "read"}, + }, + // No DefaultPolicy + } + interceptor := NewGRPCAuthStreamPolicy(auth, cfg) + + ctx := metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("authorization", "Bearer valid-token"), + ) + ss := &fakeServerStream{ctx: ctx} + + err := interceptor(nil, ss, dummyInfo, handler) + require.Error(t, err) + + st, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, codes.Internal, st.Code()) + assert.Contains(t, st.Message(), "internal configuration error") + }) + + t.Run("multi_tenant_enabled_propagates_tenant_metadata_in_stream", func(t *testing.T) { + t.Setenv("MULTI_TENANT_ENABLED", "true") + + server := mockAuthServer(t, true, http.StatusOK) + defer server.Close() + + auth := &AuthClient{ + Address: server.URL, + Enabled: true, + Logger: &testLogger{}, + } + + token := createTestJWT(jwt.MapClaims{ + "type": "normal-user", + "owner": "stream-owner", + "sub": "user1", + "tenantId": "tid-stream", + "tenantSlug": "stream-org", + }) + + defaultPol := Policy{Resource: "res", Action: "read"} + cfg := PolicyConfig{DefaultPolicy: &defaultPol} + interceptor := NewGRPCAuthStreamPolicy(auth, cfg) + + ctx := metadata.NewIncomingContext( + context.Background(), + metadata.Pairs("authorization", "Bearer "+token), + ) + ss := &fakeServerStream{ctx: ctx} + + var capturedStream grpc.ServerStream + + handler := func(_ any, ss grpc.ServerStream) error { + capturedStream = ss + return nil + } + + err := interceptor(nil, ss, dummyInfo, handler) + require.NoError(t, err) + + // Verify tenant metadata was propagated via the wrapped stream context + md, ok := metadata.FromIncomingContext(capturedStream.Context()) + require.True(t, ok) + assert.Equal(t, []string{"tid-stream"}, md.Get("md-tenant-id")) + assert.Equal(t, []string{"stream-org"}, md.Get("md-tenant-slug")) + assert.Equal(t, []string{"stream-owner"}, md.Get("md-tenant-owner")) + }) +} + +// --------------------------------------------------------------------------- +// wrappedServerStream +// --------------------------------------------------------------------------- + +func TestWrappedServerStream_Context(t *testing.T) { + t.Parallel() + + ctx := context.WithValue(context.Background(), struct{}{}, "test-value") //nolint:staticcheck // test-only context key + inner := &fakeServerStream{ctx: context.Background()} + wrapped := &wrappedServerStream{ServerStream: inner, ctx: ctx} + + assert.Equal(t, ctx, wrapped.Context()) + assert.NotEqual(t, inner.Context(), wrapped.Context()) +} From 805aadb8ff2f1a6a8547733eeaf2618220511ee7 Mon Sep 17 00:00:00 2001 From: Brecci Date: Thu, 26 Feb 2026 17:45:30 -0300 Subject: [PATCH 3/5] fix: replace deprecated commons API calls Replace NewTracerFromContext + NewHeaderIDFromContext with the unified NewTrackingFromContext across HTTP and gRPC middleware. X-Lerian-Ref: 0x1 --- auth/middleware/middleware.go | 9 +++------ auth/middleware/middlewareGRPC.go | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/auth/middleware/middleware.go b/auth/middleware/middleware.go index 7689149..2be9718 100644 --- a/auth/middleware/middleware.go +++ b/auth/middleware/middleware.go @@ -112,8 +112,7 @@ func (auth *AuthClient) Authorize(sub, resource, action string) fiber.Handler { return func(c *fiber.Ctx) error { ctx := opentelemetry.ExtractHTTPContext(c) - tracer := commons.NewTracerFromContext(ctx) - reqID := commons.NewHeaderIDFromContext(ctx) + _, tracer, reqID, _ := commons.NewTrackingFromContext(ctx) if !auth.Enabled || auth.Address == "" { return c.Next() @@ -158,8 +157,7 @@ func (auth *AuthClient) Authorize(sub, resource, action string) fiber.Handler { // checkAuthorization sends an authorization request to the external service and returns whether the action is authorized. func (auth *AuthClient) checkAuthorization(ctx context.Context, sub, resource, action, accessToken string) (bool, int, error) { - tracer := commons.NewTracerFromContext(ctx) - reqID := commons.NewHeaderIDFromContext(ctx) + _, tracer, reqID, _ := commons.NewTrackingFromContext(ctx) ctx, span := tracer.Start(ctx, "lib_auth.check_authorization") defer span.End() @@ -298,8 +296,7 @@ func (auth *AuthClient) checkAuthorization(ctx context.Context, sub, resource, a // It takes the client ID and client secret as parameters and returns the access token if the request is successful. // If the request fails at any step, an error is returned with a descriptive message. func (auth *AuthClient) GetApplicationToken(ctx context.Context, clientID, clientSecret string) (string, error) { - tracer := commons.NewTracerFromContext(ctx) - reqID := commons.NewHeaderIDFromContext(ctx) + _, tracer, reqID, _ := commons.NewTrackingFromContext(ctx) ctx, span := tracer.Start(ctx, "lib_auth.get_application_token") defer span.End() diff --git a/auth/middleware/middlewareGRPC.go b/auth/middleware/middlewareGRPC.go index 43f612a..705f0ba 100644 --- a/auth/middleware/middlewareGRPC.go +++ b/auth/middleware/middlewareGRPC.go @@ -50,8 +50,7 @@ func NewGRPCAuthUnaryPolicy(auth *AuthClient, cfg PolicyConfig) grpc.UnaryServer } token, ok := extractTokenFromMD(ctx) - tracer := commons.NewTracerFromContext(ctx) - reqID := commons.NewHeaderIDFromContext(ctx) + _, tracer, reqID, _ := commons.NewTrackingFromContext(ctx) ctx, span := tracer.Start(ctx, "lib_auth.authorize_grpc_unary_policy") defer span.End() From 4b6b5c55dd052a1e20cd32a81a66563a6ec545e4 Mon Sep 17 00:00:00 2001 From: Brecci Date: Fri, 27 Feb 2026 13:04:54 -0300 Subject: [PATCH 4/5] chore: fix lint issues --- auth/middleware/middleware.go | 8 +- auth/middleware/middleware_test.go | 36 ++++----- go.mod | 63 ++++++++-------- go.sum | 115 +++++++++++++++-------------- 4 files changed, 119 insertions(+), 103 deletions(-) diff --git a/auth/middleware/middleware.go b/auth/middleware/middleware.go index a521713..e0d22e8 100644 --- a/auth/middleware/middleware.go +++ b/auth/middleware/middleware.go @@ -51,11 +51,17 @@ const ( // If the service is healthy, it logs a successful connection message; otherwise, it logs the failure reason. func NewAuthClient(address string, enabled bool, logger *log.Logger) *AuthClient { var l log.Logger + var err error if logger != nil { l = *logger } else { - l = zap.InitializeLogger() + l, err = zap.InitializeLoggerWithError() + if err != nil { + err = fmt.Errorf("failed to initialize logger: %w", err) + + l = &log.NoneLogger{} + } } if !enabled || address == "" { diff --git a/auth/middleware/middleware_test.go b/auth/middleware/middleware_test.go index 460310b..ea6ddec 100644 --- a/auth/middleware/middleware_test.go +++ b/auth/middleware/middleware_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - "github.com/LerianStudio/lib-commons/v2/commons/log" + "github.com/LerianStudio/lib-commons/v3/commons/log" jwt "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -51,24 +51,24 @@ func mockAuthServer(t *testing.T, authorized bool, statusCode int) *httptest.Ser // testLogger is a minimal log.Logger implementation for tests that discards all output. type testLogger struct{} -func (l *testLogger) Info(_ ...any) {} -func (l *testLogger) Infof(_ string, _ ...any) {} -func (l *testLogger) Infoln(_ ...any) {} -func (l *testLogger) Error(_ ...any) {} -func (l *testLogger) Errorf(_ string, _ ...any) {} -func (l *testLogger) Errorln(_ ...any) {} -func (l *testLogger) Warn(_ ...any) {} -func (l *testLogger) Warnf(_ string, _ ...any) {} -func (l *testLogger) Warnln(_ ...any) {} -func (l *testLogger) Debug(_ ...any) {} -func (l *testLogger) Debugf(_ string, _ ...any) {} -func (l *testLogger) Debugln(_ ...any) {} -func (l *testLogger) Fatal(_ ...any) {} -func (l *testLogger) Fatalf(_ string, _ ...any) {} -func (l *testLogger) Fatalln(_ ...any) {} -func (l *testLogger) WithFields(_ ...any) log.Logger { return l } +func (l *testLogger) Info(_ ...any) {} +func (l *testLogger) Infof(_ string, _ ...any) {} +func (l *testLogger) Infoln(_ ...any) {} +func (l *testLogger) Error(_ ...any) {} +func (l *testLogger) Errorf(_ string, _ ...any) {} +func (l *testLogger) Errorln(_ ...any) {} +func (l *testLogger) Warn(_ ...any) {} +func (l *testLogger) Warnf(_ string, _ ...any) {} +func (l *testLogger) Warnln(_ ...any) {} +func (l *testLogger) Debug(_ ...any) {} +func (l *testLogger) Debugf(_ string, _ ...any) {} +func (l *testLogger) Debugln(_ ...any) {} +func (l *testLogger) Fatal(_ ...any) {} +func (l *testLogger) Fatalf(_ string, _ ...any) {} +func (l *testLogger) Fatalln(_ ...any) {} +func (l *testLogger) WithFields(_ ...any) log.Logger { return l } func (l *testLogger) WithDefaultMessageTemplate(_ string) log.Logger { return l } -func (l *testLogger) Sync() error { return nil } +func (l *testLogger) Sync() error { return nil } // --------------------------------------------------------------------------- // checkAuthorization - subject construction diff --git a/go.mod b/go.mod index 18badb7..20cbc00 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,14 @@ module github.com/LerianStudio/lib-auth/v2 -go 1.24.0 +go 1.25.0 require ( - github.com/LerianStudio/lib-commons/v3 v3.0.0-beta.2 - github.com/gofiber/fiber/v2 v2.52.11 - github.com/golang-jwt/jwt/v5 v5.3.0 - go.opentelemetry.io/otel v1.39.0 - google.golang.org/grpc v1.78.0 + github.com/LerianStudio/lib-commons/v3 v3.0.0-beta.8 + github.com/gofiber/fiber/v2 v2.52.12 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/otel v1.40.0 + google.golang.org/grpc v1.79.1 ) require github.com/google/uuid v1.6.0 // indirect @@ -17,19 +18,20 @@ require ( github.com/andybalholm/brotli v1.2.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/joho/godotenv v1.5.1 // indirect - github.com/klauspost/compress v1.18.2 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/sony/gobreaker v1.0.0 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect @@ -38,25 +40,26 @@ require ( github.com/valyala/fasthttp v1.69.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/bridges/otelzap v0.12.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect - go.opentelemetry.io/otel/log v0.13.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/otel/sdk v1.37.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.opentelemetry.io/contrib/bridges/otelzap v0.15.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/log v0.16.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect - google.golang.org/protobuf v1.36.6 // indirect + go.uber.org/zap v1.27.1 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e68b6c8..3e53a70 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/LerianStudio/lib-commons/v3 v3.0.0-beta.2 h1:ykbnpOdjQtZ736A/SicvQVPylAihrgENtM5aJ29Bpus= -github.com/LerianStudio/lib-commons/v3 v3.0.0-beta.2/go.mod h1:eegTRzYBTMBmXew279htSmBWX0n4syWT9iTDr2ZCwNo= +github.com/LerianStudio/lib-commons/v3 v3.0.0-beta.8 h1:zH/tQ+I1rUrPUB6wIUY3rJohFrp4CH7rXprMs6+RoPg= +github.com/LerianStudio/lib-commons/v3 v3.0.0-beta.8/go.mod h1:eegTRzYBTMBmXew279htSmBWX0n4syWT9iTDr2ZCwNo= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= @@ -8,10 +8,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -24,8 +22,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= -github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= +github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -34,12 +32,16 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= @@ -48,11 +50,13 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= @@ -76,34 +80,34 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/bridges/otelzap v0.14.0 h1:2nKw2ZXZOC0N8RBsBbYwGwfKR7kJWzzyCZ6QfUGW/es= -go.opentelemetry.io/contrib/bridges/otelzap v0.14.0/go.mod h1:kvyVt0WEI5BB6XaIStXPIkCSQ2nSkyd8IZnAHLEXge4= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= -go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= -go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= -go.opentelemetry.io/otel/log/logtest v0.15.0 h1:porNFuxAjodl6LhePevOc3n7bo3Wi3JhGXNWe7KP8iU= -go.opentelemetry.io/otel/log/logtest v0.15.0/go.mod h1:c8epqBXGHgS1LiNgmD+LuNYK9lSS3mqvtMdxLsfJgLg= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= -go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/contrib/bridges/otelzap v0.15.0 h1:x4qzjKkTl2hXmLl+IviSXvzaTyCJSYvpFZL5SRVLBxs= +go.opentelemetry.io/contrib/bridges/otelzap v0.15.0/go.mod h1:h7dZHJgqkzUiKFXCTJBrPWH0LEZaZXBFzKWstjWBRxw= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= +go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= +go.opentelemetry.io/otel/log/logtest v0.16.0 h1:jr1CG3Z6FD9pwUaL/D0s0X4lY2ZVm1jP3JfCtzGxUmE= +go.opentelemetry.io/otel/log/logtest v0.16.0/go.mod h1:qeeZw+cI/rAtCzZ03Kq1ozq6C4z/PCa+K+bb0eJfKNs= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= +go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -114,24 +118,27 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 h1:X9z6obt+cWRX8XjDVOn+SZWhWe5kZHm46TThU9j+jss= -google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 78150e9e7ffcbbbf163731d3b8874e2f46b31ede Mon Sep 17 00:00:00 2001 From: Brecci Date: Fri, 27 Feb 2026 13:12:38 -0300 Subject: [PATCH 5/5] chore: fix lint issues with govet --- auth/middleware/middleware.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/auth/middleware/middleware.go b/auth/middleware/middleware.go index e0d22e8..5111045 100644 --- a/auth/middleware/middleware.go +++ b/auth/middleware/middleware.go @@ -51,6 +51,7 @@ const ( // If the service is healthy, it logs a successful connection message; otherwise, it logs the failure reason. func NewAuthClient(address string, enabled bool, logger *log.Logger) *AuthClient { var l log.Logger + var err error if logger != nil { @@ -58,9 +59,9 @@ func NewAuthClient(address string, enabled bool, logger *log.Logger) *AuthClient } else { l, err = zap.InitializeLoggerWithError() if err != nil { - err = fmt.Errorf("failed to initialize logger: %w", err) - l = &log.NoneLogger{} + + l.Errorf("failed to initialize logger, using NoneLogger: %v\n", err) } }