diff --git a/auth/middleware/middleware.go b/auth/middleware/middleware.go index d91b32a..5111045 100644 --- a/auth/middleware/middleware.go +++ b/auth/middleware/middleware.go @@ -52,10 +52,17 @@ const ( 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 { + l = &log.NoneLogger{} + + l.Errorf("failed to initialize logger, using NoneLogger: %v\n", err) + } } if !enabled || address == "" { @@ -112,8 +119,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 +164,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 +303,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 f78481d..15a0aa2 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/v3/commons" "github.com/LerianStudio/lib-commons/v3/commons/opentelemetry" + jwt "github.com/golang-jwt/jwt/v5" "go.opentelemetry.io/otel/attribute" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -47,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() @@ -97,6 +99,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 +202,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 new file mode 100644 index 0000000..e092375 --- /dev/null +++ b/auth/middleware/middlewareGRPC_test.go @@ -0,0 +1,927 @@ +package middleware + +import ( + "context" + "net/http" + "testing" + + jwt "github.com/golang-jwt/jwt/v5" + "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 +} + +// --------------------------------------------------------------------------- +// 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()) +} diff --git a/auth/middleware/middleware_test.go b/auth/middleware/middleware_test.go new file mode 100644 index 0000000..ea6ddec --- /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/v3/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 17b572f..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.uber.org/mock v0.5.2 // 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=