From 2964b9a07f92313cd8cc281c1dc9cb52185bcfc3 Mon Sep 17 00:00:00 2001 From: Flo Date: Thu, 19 Feb 2026 00:13:43 +0100 Subject: [PATCH 1/8] feat: key-sentinel-middleware --- dev/k8s/manifests/cilium-policies.yaml | 9 + dev/k8s/manifests/sentinel.yaml | 1 + pkg/codes/unkey_sentinel.go | 24 + pkg/counter/redis.go | 14 +- pkg/zen/middleware_logger.go | 37 +- svc/api/routes/register.go | 2 +- svc/frontline/routes/register.go | 4 +- svc/krane/internal/sentinel/apply.go | 3 + svc/sentinel/BUILD.bazel | 7 + svc/sentinel/config.go | 13 + svc/sentinel/engine/BUILD.bazel | 55 +++ svc/sentinel/engine/engine.go | 146 ++++++ svc/sentinel/engine/engine_test.go | 90 ++++ svc/sentinel/engine/integration_test.go | 552 ++++++++++++++++++++++ svc/sentinel/engine/keyauth.go | 169 +++++++ svc/sentinel/engine/keyextract.go | 72 +++ svc/sentinel/engine/keyextract_test.go | 175 +++++++ svc/sentinel/engine/match.go | 178 +++++++ svc/sentinel/engine/match_test.go | 311 ++++++++++++ svc/sentinel/middleware/error_handling.go | 7 + svc/sentinel/middleware/observability.go | 28 ++ svc/sentinel/routes/BUILD.bazel | 1 + svc/sentinel/routes/proxy/BUILD.bazel | 1 + svc/sentinel/routes/proxy/handler.go | 23 + svc/sentinel/routes/register.go | 3 +- svc/sentinel/routes/services.go | 2 + svc/sentinel/run.go | 86 ++++ 27 files changed, 2005 insertions(+), 8 deletions(-) create mode 100644 svc/sentinel/engine/BUILD.bazel create mode 100644 svc/sentinel/engine/engine.go create mode 100644 svc/sentinel/engine/engine_test.go create mode 100644 svc/sentinel/engine/integration_test.go create mode 100644 svc/sentinel/engine/keyauth.go create mode 100644 svc/sentinel/engine/keyextract.go create mode 100644 svc/sentinel/engine/keyextract_test.go create mode 100644 svc/sentinel/engine/match.go create mode 100644 svc/sentinel/engine/match_test.go diff --git a/dev/k8s/manifests/cilium-policies.yaml b/dev/k8s/manifests/cilium-policies.yaml index 5657fd7b92..2537b03009 100644 --- a/dev/k8s/manifests/cilium-policies.yaml +++ b/dev/k8s/manifests/cilium-policies.yaml @@ -175,6 +175,15 @@ spec: protocol: TCP - port: "9000" protocol: TCP + # Redis in unkey namespace (rate limiting, usage limiting) + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: unkey + app: redis + toPorts: + - ports: + - port: "6379" + protocol: TCP --- # 6. Allow customer pods to reach Krane for secret decryption # Customer pods need to call Krane's DecryptSecretsBlob RPC during init (inject container) diff --git a/dev/k8s/manifests/sentinel.yaml b/dev/k8s/manifests/sentinel.yaml index c3e2f40bdf..3d2a8e8150 100644 --- a/dev/k8s/manifests/sentinel.yaml +++ b/dev/k8s/manifests/sentinel.yaml @@ -13,6 +13,7 @@ stringData: UNKEY_DATABASE_PRIMARY: "unkey:password@tcp(mysql.unkey.svc.cluster.local:3306)/unkey?parseTime=true&interpolateParams=true" UNKEY_DATABASE_REPLICA: "unkey:password@tcp(mysql.unkey.svc.cluster.local:3306)/unkey?parseTime=true&interpolateParams=true" UNKEY_CLICKHOUSE_URL: "clickhouse://default:password@clickhouse.unkey.svc.cluster.local:9000?secure=false&skip_verify=true" + UNKEY_REDIS_URL: "redis://default:password@redis.unkey.svc.cluster.local:6379" --- # Role allowing secret access in sentinel namespace - only bound to krane apiVersion: rbac.authorization.k8s.io/v1 diff --git a/pkg/codes/unkey_sentinel.go b/pkg/codes/unkey_sentinel.go index e8743b2916..555bb3befa 100644 --- a/pkg/codes/unkey_sentinel.go +++ b/pkg/codes/unkey_sentinel.go @@ -36,6 +36,21 @@ type sentinelInternal struct { InvalidConfiguration Code } +// sentinelAuth defines errors related to sentinel authentication and authorization. +type sentinelAuth struct { + // MissingCredentials represents a 401 error - no credentials found in request + MissingCredentials Code + + // InvalidKey represents a 401 error - key not found, disabled, or expired + InvalidKey Code + + // InsufficientPermissions represents a 403 error - key lacks required permissions + InsufficientPermissions Code + + // RateLimited represents a 429 error - rate limit exceeded + RateLimited Code +} + // UnkeySentinelErrors defines all sentinel-related errors in the Unkey system. // These errors occur when the sentinel service has issues routing requests to instances. type UnkeySentinelErrors struct { @@ -47,6 +62,9 @@ type UnkeySentinelErrors struct { // Internal contains errors related to internal sentinel functionality. Internal sentinelInternal + + // Auth contains errors related to sentinel authentication and authorization. + Auth sentinelAuth } // Sentinel contains all predefined sentinel error codes. @@ -68,4 +86,10 @@ var Sentinel = UnkeySentinelErrors{ InternalServerError: Code{SystemUnkey, CategoryInternalServerError, "internal_server_error"}, InvalidConfiguration: Code{SystemUnkey, CategoryInternalServerError, "invalid_configuration"}, }, + Auth: sentinelAuth{ + MissingCredentials: Code{SystemSentinel, CategoryUnauthorized, "missing_credentials"}, + InvalidKey: Code{SystemSentinel, CategoryUnauthorized, "invalid_key"}, + InsufficientPermissions: Code{SystemSentinel, CategoryForbidden, "insufficient_permissions"}, + RateLimited: Code{SystemSentinel, CategoryRateLimited, "rate_limited"}, + }, } diff --git a/pkg/counter/redis.go b/pkg/counter/redis.go index b0a788a6ac..ea7cc94b0a 100644 --- a/pkg/counter/redis.go +++ b/pkg/counter/redis.go @@ -88,11 +88,21 @@ func NewRedis(config RedisConfig) (Counter, error) { return nil, fmt.Errorf("failed to parse redis url: %w", err) } + // Use aggressive timeouts so requests fail fast when Redis is slow or + // unreachable, rather than blocking for the go-redis defaults (5s dial, + // 3s read/write). See https://github.com/unkeyed/unkey/issues/4891. + opts.DialTimeout = 1 * time.Second + opts.ReadTimeout = 500 * time.Millisecond + opts.WriteTimeout = 500 * time.Millisecond + rdb := redis.NewClient(opts) logger.Debug("pinging redis") - // Test connection - _, err = rdb.Ping(context.Background()).Result() + // Test connection with a bounded timeout + pingCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + _, err = rdb.Ping(pingCtx).Result() if err != nil { return nil, fmt.Errorf("failed to ping redis: %w", err) } diff --git a/pkg/zen/middleware_logger.go b/pkg/zen/middleware_logger.go index 5ccc2c7821..6cd8e25406 100644 --- a/pkg/zen/middleware_logger.go +++ b/pkg/zen/middleware_logger.go @@ -4,22 +4,55 @@ import ( "context" "fmt" "log/slog" + "strings" "github.com/unkeyed/unkey/pkg/logger" ) +// LoggingOption configures the WithLogging middleware. +type LoggingOption func(*loggingConfig) + +type loggingConfig struct { + skipPrefixes []string +} + +// SkipPaths configures path prefixes that should not be logged. +// Any request whose path starts with one of these prefixes will +// skip logging entirely. +// +// Example: +// +// zen.WithLogging(zen.SkipPaths("/_unkey/internal/", "/health/")) +func SkipPaths(prefixes ...string) LoggingOption { + return func(cfg *loggingConfig) { + cfg.skipPrefixes = append(cfg.skipPrefixes, prefixes...) + } +} + // WithLogging returns middleware that logs information about each request. // It captures the method, path, status code, and processing time. // // Example: // // server.RegisterRoute( -// []zen.Middleware{zen.WithLogging()}, +// []zen.Middleware{zen.WithLogging(zen.SkipPaths("/_unkey/internal/", "/health/"))}, // route, // ) -func WithLogging() Middleware { +func WithLogging(opts ...LoggingOption) Middleware { + cfg := &loggingConfig{ + skipPrefixes: nil, + } + for _, opt := range opts { + opt(cfg) + } + return func(next HandleFunc) HandleFunc { return func(ctx context.Context, s *Session) error { + for _, prefix := range cfg.skipPrefixes { + if strings.HasPrefix(s.r.URL.Path, prefix) { + return next(ctx, s) + } + } ctx, event := logger.StartWideEvent(ctx, fmt.Sprintf("%s %s", s.r.Method, s.r.URL.Path), diff --git a/svc/api/routes/register.go b/svc/api/routes/register.go index 4785eaedda..01cb98709a 100644 --- a/svc/api/routes/register.go +++ b/svc/api/routes/register.go @@ -81,7 +81,7 @@ import ( func Register(srv *zen.Server, svc *Services, info zen.InstanceInfo) { withObservability := zen.WithObservability() withMetrics := zen.WithMetrics(svc.ClickHouse, info) - withLogging := zen.WithLogging() + withLogging := zen.WithLogging(zen.SkipPaths("/_unkey/internal/", "/health/")) withPanicRecovery := zen.WithPanicRecovery() withErrorHandling := middleware.WithErrorHandling() withValidation := zen.WithValidation(svc.Validator) diff --git a/svc/frontline/routes/register.go b/svc/frontline/routes/register.go index a15eb54962..360aa0faf3 100644 --- a/svc/frontline/routes/register.go +++ b/svc/frontline/routes/register.go @@ -12,7 +12,7 @@ import ( // Register registers all frontline routes for the HTTPS server func Register(srv *zen.Server, svc *Services) { - withLogging := zen.WithLogging() + withLogging := zen.WithLogging(zen.SkipPaths("/_unkey/internal/", "/health/")) withPanicRecovery := zen.WithPanicRecovery() withObservability := middleware.WithObservability(svc.Region) withTimeout := zen.WithTimeout(5 * time.Minute) @@ -43,7 +43,7 @@ func Register(srv *zen.Server, svc *Services) { // RegisterChallengeServer registers routes for the HTTP challenge server (Let's Encrypt ACME) func RegisterChallengeServer(srv *zen.Server, svc *Services) { - withLogging := zen.WithLogging() + withLogging := zen.WithLogging(zen.SkipPaths("/_unkey/internal/", "/health/")) // Health check endpoint srv.RegisterRoute( diff --git a/svc/krane/internal/sentinel/apply.go b/svc/krane/internal/sentinel/apply.go index ae7cbcab49..17092de394 100644 --- a/svc/krane/internal/sentinel/apply.go +++ b/svc/krane/internal/sentinel/apply.go @@ -139,6 +139,9 @@ func (c *Controller) ensureSentinelExists(ctx context.Context, sentinel *ctrlv1. ClickHouse: sentinelcfg.ClickHouseConfig{ URL: "${UNKEY_CLICKHOUSE_URL}", }, + Redis: sentinelcfg.RedisConfig{ + URL: "${UNKEY_REDIS_URL}", + }, Observability: config.Observability{ Logging: &config.LoggingConfig{ SampleRate: 1.0, diff --git a/svc/sentinel/BUILD.bazel b/svc/sentinel/BUILD.bazel index a2e7d662a4..3532bdb05c 100644 --- a/svc/sentinel/BUILD.bazel +++ b/svc/sentinel/BUILD.bazel @@ -9,18 +9,25 @@ go_library( importpath = "github.com/unkeyed/unkey/svc/sentinel", visibility = ["//visibility:public"], deps = [ + "//internal/services/keys", + "//internal/services/ratelimit", + "//internal/services/usagelimiter", + "//pkg/cache", "//pkg/cache/clustering", "//pkg/clickhouse", "//pkg/clock", "//pkg/cluster", "//pkg/config", + "//pkg/counter", "//pkg/db", "//pkg/logger", "//pkg/otel", "//pkg/prometheus", + "//pkg/rbac", "//pkg/runner", "//pkg/version", "//pkg/zen", + "//svc/sentinel/engine", "//svc/sentinel/routes", "//svc/sentinel/services/router", ], diff --git a/svc/sentinel/config.go b/svc/sentinel/config.go index 397051c629..1eb0f053c0 100644 --- a/svc/sentinel/config.go +++ b/svc/sentinel/config.go @@ -15,6 +15,15 @@ type ClickHouseConfig struct { URL string `toml:"url"` } +// RedisConfig configures the Redis connection used for rate limiting +// and usage limiting in sentinel middleware policies. +type RedisConfig struct { + // URL is the Redis connection string. + // Example: "redis://default:password@redis:6379" + // When empty, the middleware engine (KeyAuth, rate limiting) is disabled. + URL string `toml:"url"` +} + // Config holds the complete configuration for the Sentinel server. It is // designed to be loaded from a TOML file using [config.Load]: // @@ -53,6 +62,10 @@ type Config struct { // ClickHouse configures analytics storage. See [ClickHouseConfig]. ClickHouse ClickHouseConfig `toml:"clickhouse"` + // Redis configures the Redis connection for rate limiting and usage limiting. + // Required when sentinel middleware policies use KeyAuth with auto-applied rate limits. + Redis RedisConfig `toml:"redis"` + // Gossip configures distributed cache invalidation. See [config.GossipConfig]. // When nil (section omitted), gossip is disabled and invalidation is local-only. Gossip *config.GossipConfig `toml:"gossip"` diff --git a/svc/sentinel/engine/BUILD.bazel b/svc/sentinel/engine/BUILD.bazel new file mode 100644 index 0000000000..07dc9ed321 --- /dev/null +++ b/svc/sentinel/engine/BUILD.bazel @@ -0,0 +1,55 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "engine", + srcs = [ + "engine.go", + "keyauth.go", + "keyextract.go", + "match.go", + ], + importpath = "github.com/unkeyed/unkey/svc/sentinel/engine", + visibility = ["//visibility:public"], + deps = [ + "//gen/proto/sentinel/v1:sentinel", + "//internal/services/keys", + "//pkg/clock", + "//pkg/codes", + "//pkg/fault", + "//pkg/hash", + "//pkg/logger", + "//pkg/rbac", + "//pkg/zen", + "@org_golang_google_protobuf//encoding/protojson", + ], +) + +go_test( + name = "engine_test", + srcs = [ + "engine_test.go", + "integration_test.go", + "keyextract_test.go", + "match_test.go", + ], + embed = [":engine"], + deps = [ + "//gen/proto/sentinel/v1:sentinel", + "//internal/services/keys", + "//internal/services/ratelimit", + "//internal/services/usagelimiter", + "//pkg/cache", + "//pkg/clickhouse", + "//pkg/clock", + "//pkg/counter", + "//pkg/db", + "//pkg/dockertest", + "//pkg/hash", + "//pkg/rbac", + "//pkg/uid", + "//pkg/zen", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + "@org_golang_google_protobuf//encoding/protojson", + ], +) diff --git a/svc/sentinel/engine/engine.go b/svc/sentinel/engine/engine.go new file mode 100644 index 0000000000..e812d1c22c --- /dev/null +++ b/svc/sentinel/engine/engine.go @@ -0,0 +1,146 @@ +package engine + +import ( + "context" + "net/http" + + sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" + "github.com/unkeyed/unkey/internal/services/keys" + "github.com/unkeyed/unkey/pkg/clock" + "github.com/unkeyed/unkey/pkg/logger" + "github.com/unkeyed/unkey/pkg/zen" + "google.golang.org/protobuf/encoding/protojson" +) + +// PrincipalHeader is the header name used to pass the authenticated principal +// to upstream services. +const PrincipalHeader = "X-Unkey-Principal" + +// Config holds the configuration for creating a new Engine. +type Config struct { + KeyService keys.KeyService + Clock clock.Clock +} + +// Evaluator evaluates sentinel middleware policies against incoming requests. +type Evaluator interface { + Evaluate(ctx context.Context, sess *zen.Session, req *http.Request, mw *sentinelv1.Middleware) (Result, error) +} + +// Engine implements Evaluator. +type Engine struct { + keyAuth *KeyAuthExecutor + regexCache *regexCache +} + +var _ Evaluator = (*Engine)(nil) + +// Result holds the outcome of middleware evaluation. +type Result struct { + Principal *sentinelv1.Principal +} + +// New creates a new Engine with the given configuration. +func New(cfg Config) *Engine { + return &Engine{ + keyAuth: &KeyAuthExecutor{ + keyService: cfg.KeyService, + clock: cfg.Clock, + }, + regexCache: newRegexCache(), + } +} + +// ParseMiddleware performs lenient deserialization of sentinel_config bytes into +// a Middleware proto. Returns nil for empty, legacy empty-object, or malformed data +// to allow plain pass-through proxying. +func ParseMiddleware(raw []byte) *sentinelv1.Middleware { + if len(raw) == 0 || string(raw) == "{}" { + return nil + } + + mw := &sentinelv1.Middleware{} + if err := protojson.Unmarshal(raw, mw); err != nil { + logger.Warn("failed to unmarshal sentinel middleware config, treating as pass-through", + "error", err.Error(), + ) + + return nil + } + + if len(mw.GetPolicies()) == 0 { + return nil + } + + return mw +} + +// Evaluate processes all middleware policies against the incoming request. +// Policies are evaluated in order. Disabled policies are skipped. +// Authentication policies produce a Principal; the first successful auth sets it. +func (e *Engine) Evaluate( + ctx context.Context, + sess *zen.Session, + req *http.Request, + mw *sentinelv1.Middleware, +) (Result, error) { + var result Result + + for _, policy := range mw.GetPolicies() { + if !policy.GetEnabled() { + continue + } + + // Check match expressions + matched, err := matchesRequest(req, policy.GetMatch(), e.regexCache) + if err != nil { + return result, err + } + + if !matched { + continue + } + + // Dispatch by config type + switch cfg := policy.GetConfig().(type) { + case *sentinelv1.Policy_Keyauth: + // Skip if we already have a principal from a previous auth policy + if result.Principal != nil { + continue + } + + principal, execErr := e.keyAuth.Execute(ctx, sess, req, cfg.Keyauth) + if execErr != nil { + return result, execErr + } + + if principal != nil { + result.Principal = principal + } + + // Future policy types will be added here: + // case *sentinelv1.Policy_Jwtauth: + // case *sentinelv1.Policy_Basicauth: + // case *sentinelv1.Policy_Ratelimit: + // case *sentinelv1.Policy_IpRules: + // case *sentinelv1.Policy_Openapi: + + default: + // Unknown policy type — skip silently for forward compatibility + continue + } + } + + return result, nil +} + +// SerializePrincipal converts a Principal to a JSON string for use in the +// X-Unkey-Principal header. +func SerializePrincipal(p *sentinelv1.Principal) (string, error) { + b, err := protojson.Marshal(p) + if err != nil { + return "", err + } + + return string(b), nil +} diff --git a/svc/sentinel/engine/engine_test.go b/svc/sentinel/engine/engine_test.go new file mode 100644 index 0000000000..c9ac830300 --- /dev/null +++ b/svc/sentinel/engine/engine_test.go @@ -0,0 +1,90 @@ +package engine + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" + "google.golang.org/protobuf/encoding/protojson" +) + +func TestParseMiddleware_Nil(t *testing.T) { + t.Parallel() + assert.Nil(t, ParseMiddleware(nil)) +} + +func TestParseMiddleware_Empty(t *testing.T) { + t.Parallel() + assert.Nil(t, ParseMiddleware([]byte{})) +} + +func TestParseMiddleware_EmptyJSON(t *testing.T) { + t.Parallel() + assert.Nil(t, ParseMiddleware([]byte("{}"))) +} + +func TestParseMiddleware_InvalidProto(t *testing.T) { + t.Parallel() + assert.Nil(t, ParseMiddleware([]byte("not a valid protobuf"))) +} + +func TestParseMiddleware_NoPolicies(t *testing.T) { + t.Parallel() + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: nil, + } + raw, err := protojson.Marshal(mw) + require.NoError(t, err) + assert.Nil(t, ParseMiddleware(raw)) +} + +func TestParseMiddleware_WithPolicies(t *testing.T) { + t.Parallel() + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: []*sentinelv1.Policy{ + { + Id: "p1", + Name: "key auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceId: "ks_123"}, + }, + }, + }, + } + raw, err := protojson.Marshal(mw) + require.NoError(t, err) + + result := ParseMiddleware(raw) + require.NotNil(t, result) + assert.Len(t, result.GetPolicies(), 1) + assert.Equal(t, "p1", result.GetPolicies()[0].GetId()) +} + +func TestSerializePrincipal(t *testing.T) { + t.Parallel() + //nolint:exhaustruct + p := &sentinelv1.Principal{ + Subject: "user_123", + Type: sentinelv1.PrincipalType_PRINCIPAL_TYPE_API_KEY, + Claims: map[string]string{ + "key_id": "key_abc", + "workspace_id": "ws_456", + }, + } + + s, err := SerializePrincipal(p) + require.NoError(t, err) + + // Round-trip: unmarshal back into a Principal and compare + var roundTripped sentinelv1.Principal + err = protojson.Unmarshal([]byte(s), &roundTripped) + require.NoError(t, err) + assert.Equal(t, "user_123", roundTripped.GetSubject()) + assert.Equal(t, sentinelv1.PrincipalType_PRINCIPAL_TYPE_API_KEY, roundTripped.GetType()) + assert.Equal(t, "key_abc", roundTripped.GetClaims()["key_id"]) + assert.Equal(t, "ws_456", roundTripped.GetClaims()["workspace_id"]) +} diff --git a/svc/sentinel/engine/integration_test.go b/svc/sentinel/engine/integration_test.go new file mode 100644 index 0000000000..ebd3663358 --- /dev/null +++ b/svc/sentinel/engine/integration_test.go @@ -0,0 +1,552 @@ +package engine_test + +import ( + "context" + "database/sql" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" + "github.com/unkeyed/unkey/internal/services/keys" + "github.com/unkeyed/unkey/internal/services/ratelimit" + "github.com/unkeyed/unkey/internal/services/usagelimiter" + "github.com/unkeyed/unkey/pkg/cache" + "github.com/unkeyed/unkey/pkg/clickhouse" + "github.com/unkeyed/unkey/pkg/clock" + "github.com/unkeyed/unkey/pkg/counter" + "github.com/unkeyed/unkey/pkg/db" + "github.com/unkeyed/unkey/pkg/dockertest" + "github.com/unkeyed/unkey/pkg/hash" + "github.com/unkeyed/unkey/pkg/rbac" + "github.com/unkeyed/unkey/pkg/uid" + "github.com/unkeyed/unkey/pkg/zen" + "github.com/unkeyed/unkey/svc/sentinel/engine" +) + +// testHarness holds all real services needed for integration tests. +type testHarness struct { + t *testing.T + db db.Database + keyService keys.KeyService + engine *engine.Engine + clk clock.Clock +} + +func newTestHarness(t *testing.T) *testHarness { + t.Helper() + + mysqlCfg := dockertest.MySQL(t) + redisURL := dockertest.Redis(t) + + clk := clock.New() + + database, err := db.New(db.Config{ + PrimaryDSN: mysqlCfg.DSN, + ReadOnlyDSN: "", + }) + require.NoError(t, err) + t.Cleanup(func() { _ = database.Close() }) + + redisCounter, err := counter.NewRedis(counter.RedisConfig{ + RedisURL: redisURL, + }) + require.NoError(t, err) + t.Cleanup(func() { _ = redisCounter.Close() }) + + rateLimiter, err := ratelimit.New(ratelimit.Config{ + Clock: clk, + Counter: redisCounter, + }) + require.NoError(t, err) + t.Cleanup(func() { _ = rateLimiter.Close() }) + + usageLimiter, err := usagelimiter.NewCounter(usagelimiter.CounterConfig{ + DB: database, + Counter: redisCounter, + TTL: 60 * time.Second, + ReplayWorkers: 2, + }) + require.NoError(t, err) + t.Cleanup(func() { _ = usageLimiter.Close() }) + + keyCache, err := cache.New[string, db.CachedKeyData](cache.Config[string, db.CachedKeyData]{ + Fresh: 10 * time.Second, + Stale: 10 * time.Minute, + MaxSize: 1000, + Resource: "test_key_cache", + Clock: clk, + }) + require.NoError(t, err) + + keyService, err := keys.New(keys.Config{ + DB: database, + RateLimiter: rateLimiter, + RBAC: rbac.New(), + Clickhouse: clickhouse.NewNoop(), + Region: "test", + UsageLimiter: usageLimiter, + KeyCache: keyCache, + }) + require.NoError(t, err) + + eng := engine.New(engine.Config{ + KeyService: keyService, + Clock: clk, + }) + + return &testHarness{ + t: t, + db: database, + keyService: keyService, + engine: eng, + clk: clk, + } +} + +// seedResult holds the IDs/values created during seeding. +type seedResult struct { + WorkspaceID string + KeySpaceID string + ApiID string + KeyID string + RawKey string // the unhashed key value to use in requests +} + +// seed creates a workspace, key space, API, and key in the database. +func (h *testHarness) seed(ctx context.Context) seedResult { + h.t.Helper() + + now := time.Now().UnixMilli() + wsID := uid.New("test_ws") + orgID := uid.New("test_org") + + err := db.Query.InsertWorkspace(ctx, h.db.RW(), db.InsertWorkspaceParams{ + ID: wsID, + OrgID: orgID, + Name: uid.New("test_name"), + Slug: uid.New("slug"), + CreatedAt: now, + K8sNamespace: sql.NullString{Valid: true, String: uid.New("ns")}, + }) + require.NoError(h.t, err) + + ksID := uid.New(uid.KeySpacePrefix) + err = db.Query.InsertKeySpace(ctx, h.db.RW(), db.InsertKeySpaceParams{ + ID: ksID, + WorkspaceID: wsID, + CreatedAtM: now, + StoreEncryptedKeys: false, + DefaultPrefix: sql.NullString{Valid: false}, + DefaultBytes: sql.NullInt32{Valid: false}, + }) + require.NoError(h.t, err) + + apiID := uid.New("api") + err = db.Query.InsertApi(ctx, h.db.RW(), db.InsertApiParams{ + ID: apiID, + Name: "test-api", + WorkspaceID: wsID, + AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, + IpWhitelist: sql.NullString{Valid: false}, + KeyAuthID: sql.NullString{Valid: true, String: ksID}, + CreatedAtM: now, + }) + require.NoError(h.t, err) + + rawKey := uid.New("sk_live") + keyID := uid.New(uid.KeyPrefix) + err = db.Query.InsertKey(ctx, h.db.RW(), db.InsertKeyParams{ + ID: keyID, + KeySpaceID: ksID, + Hash: hash.Sha256(rawKey), + Start: rawKey[:8], + WorkspaceID: wsID, + ForWorkspaceID: sql.NullString{Valid: false}, + Name: sql.NullString{String: "test-key", Valid: true}, + IdentityID: sql.NullString{Valid: false}, + Meta: sql.NullString{Valid: false}, + Expires: sql.NullTime{Valid: false}, + CreatedAtM: now, + Enabled: true, + RemainingRequests: sql.NullInt32{Valid: false}, + RefillDay: sql.NullInt16{Valid: false}, + RefillAmount: sql.NullInt32{Valid: false}, + PendingMigrationID: sql.NullString{Valid: false}, + }) + require.NoError(h.t, err) + + return seedResult{ + WorkspaceID: wsID, + KeySpaceID: ksID, + ApiID: apiID, + KeyID: keyID, + RawKey: rawKey, + } +} + +// seedDisabledKey creates a key that is disabled. +func (h *testHarness) seedDisabledKey(ctx context.Context, wsID, ksID string) seedResult { + h.t.Helper() + + rawKey := uid.New("sk_live") + keyID := uid.New(uid.KeyPrefix) + err := db.Query.InsertKey(ctx, h.db.RW(), db.InsertKeyParams{ + ID: keyID, + KeySpaceID: ksID, + Hash: hash.Sha256(rawKey), + Start: rawKey[:8], + WorkspaceID: wsID, + ForWorkspaceID: sql.NullString{Valid: false}, + Name: sql.NullString{String: "disabled-key", Valid: true}, + IdentityID: sql.NullString{Valid: false}, + Meta: sql.NullString{Valid: false}, + Expires: sql.NullTime{Valid: false}, + CreatedAtM: time.Now().UnixMilli(), + Enabled: false, + RemainingRequests: sql.NullInt32{Valid: false}, + RefillDay: sql.NullInt16{Valid: false}, + RefillAmount: sql.NullInt32{Valid: false}, + PendingMigrationID: sql.NullString{Valid: false}, + }) + require.NoError(h.t, err) + + return seedResult{ + WorkspaceID: wsID, + KeySpaceID: ksID, + KeyID: keyID, + RawKey: rawKey, + } +} + +// seedKeyWithIdentity creates a key linked to an identity with an external ID. +func (h *testHarness) seedKeyWithIdentity(ctx context.Context, wsID, ksID string) seedResult { + h.t.Helper() + + now := time.Now().UnixMilli() + externalID := uid.New("ext") + identityID := uid.New("id") + + err := db.Query.InsertIdentity(ctx, h.db.RW(), db.InsertIdentityParams{ + ID: identityID, + ExternalID: externalID, + WorkspaceID: wsID, + Environment: "", + CreatedAt: now, + Meta: []byte("{}"), + }) + require.NoError(h.t, err) + + rawKey := uid.New("sk_live") + keyID := uid.New(uid.KeyPrefix) + err = db.Query.InsertKey(ctx, h.db.RW(), db.InsertKeyParams{ + ID: keyID, + KeySpaceID: ksID, + Hash: hash.Sha256(rawKey), + Start: rawKey[:8], + WorkspaceID: wsID, + ForWorkspaceID: sql.NullString{Valid: false}, + Name: sql.NullString{String: "identity-key", Valid: true}, + IdentityID: sql.NullString{String: identityID, Valid: true}, + Meta: sql.NullString{Valid: false}, + Expires: sql.NullTime{Valid: false}, + CreatedAtM: now, + Enabled: true, + RemainingRequests: sql.NullInt32{Valid: false}, + RefillDay: sql.NullInt16{Valid: false}, + RefillAmount: sql.NullInt32{Valid: false}, + PendingMigrationID: sql.NullString{Valid: false}, + }) + require.NoError(h.t, err) + + return seedResult{ + WorkspaceID: wsID, + KeySpaceID: ksID, + KeyID: keyID, + RawKey: rawKey, + } +} + +func newSession(t *testing.T, req *http.Request) *zen.Session { + t.Helper() + w := httptest.NewRecorder() + //nolint:exhaustruct + sess := &zen.Session{} + err := sess.Init(w, req, 0) + require.NoError(t, err) + return sess +} + +// --- KeyAuth integration tests --- + +func TestKeyAuth_ValidKey(t *testing.T) { + h := newTestHarness(t) + ctx := context.Background() + s := h.seed(ctx) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+s.RawKey) + sess := newSession(t, req) + + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceId: s.KeySpaceID}, + }, + }, + }, + } + + result, err := h.engine.Evaluate(ctx, sess, req, mw) + require.NoError(t, err) + require.NotNil(t, result.Principal) + + // Subject falls back to key ID when no external ID is set + assert.Equal(t, s.KeyID, result.Principal.Subject) + assert.Equal(t, sentinelv1.PrincipalType_PRINCIPAL_TYPE_API_KEY, result.Principal.Type) + assert.Equal(t, s.KeyID, result.Principal.Claims["key_id"]) + assert.Equal(t, s.WorkspaceID, result.Principal.Claims["workspace_id"]) +} + +func TestKeyAuth_ValidKey_WithIdentity(t *testing.T) { + h := newTestHarness(t) + ctx := context.Background() + base := h.seed(ctx) + s := h.seedKeyWithIdentity(ctx, base.WorkspaceID, base.KeySpaceID) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+s.RawKey) + sess := newSession(t, req) + + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceId: s.KeySpaceID}, + }, + }, + }, + } + + result, err := h.engine.Evaluate(ctx, sess, req, mw) + require.NoError(t, err) + require.NotNil(t, result.Principal) + + // Subject should be the external ID from the identity + assert.NotEqual(t, s.KeyID, result.Principal.Subject) + assert.NotEmpty(t, result.Principal.Claims["identity_id"]) + assert.NotEmpty(t, result.Principal.Claims["external_id"]) +} + +func TestKeyAuth_MissingKey_Reject(t *testing.T) { + h := newTestHarness(t) + ctx := context.Background() + s := h.seed(ctx) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + // No Authorization header + sess := newSession(t, req) + + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{ + KeySpaceId: s.KeySpaceID, + AllowAnonymous: false, + }, + }, + }, + }, + } + + _, err := h.engine.Evaluate(ctx, sess, req, mw) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing API key") +} + +func TestKeyAuth_MissingKey_AllowAnonymous(t *testing.T) { + h := newTestHarness(t) + ctx := context.Background() + s := h.seed(ctx) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + // No Authorization header + sess := newSession(t, req) + + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{ + KeySpaceId: s.KeySpaceID, + AllowAnonymous: true, + }, + }, + }, + }, + } + + result, err := h.engine.Evaluate(ctx, sess, req, mw) + require.NoError(t, err) + assert.Nil(t, result.Principal) +} + +func TestKeyAuth_InvalidKey_NotFound(t *testing.T) { + h := newTestHarness(t) + ctx := context.Background() + s := h.seed(ctx) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer sk_this_key_does_not_exist") + sess := newSession(t, req) + + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceId: s.KeySpaceID}, + }, + }, + }, + } + + _, err := h.engine.Evaluate(ctx, sess, req, mw) + require.Error(t, err) +} + +func TestKeyAuth_InvalidKey_Disabled(t *testing.T) { + h := newTestHarness(t) + ctx := context.Background() + base := h.seed(ctx) + disabled := h.seedDisabledKey(ctx, base.WorkspaceID, base.KeySpaceID) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+disabled.RawKey) + sess := newSession(t, req) + + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceId: base.KeySpaceID}, + }, + }, + }, + } + + _, err := h.engine.Evaluate(ctx, sess, req, mw) + require.Error(t, err) +} + +func TestKeyAuth_WrongKeySpace(t *testing.T) { + h := newTestHarness(t) + ctx := context.Background() + s := h.seed(ctx) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+s.RawKey) + sess := newSession(t, req) + + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceId: "ks_wrong_space"}, + }, + }, + }, + } + + _, err := h.engine.Evaluate(ctx, sess, req, mw) + require.Error(t, err) + assert.Contains(t, err.Error(), "key does not belong to expected key space") +} + +// --- Engine Evaluate integration tests --- + +func TestEvaluate_DisabledPoliciesSkipped(t *testing.T) { + h := newTestHarness(t) + ctx := context.Background() + s := h.seed(ctx) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+s.RawKey) + sess := newSession(t, req) + + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: []*sentinelv1.Policy{ + { + Id: "disabled", + Enabled: false, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceId: s.KeySpaceID}, + }, + }, + }, + } + + result, err := h.engine.Evaluate(ctx, sess, req, mw) + require.NoError(t, err) + assert.Nil(t, result.Principal) +} + +func TestEvaluate_MatchFiltering(t *testing.T) { + h := newTestHarness(t) + ctx := context.Background() + s := h.seed(ctx) + + // Request to /health doesn't match /api prefix + req := httptest.NewRequest(http.MethodGet, "/health", nil) + req.Header.Set("Authorization", "Bearer "+s.RawKey) + sess := newSession(t, req) + + //nolint:exhaustruct + mw := &sentinelv1.Middleware{ + Policies: []*sentinelv1.Policy{ + { + Id: "api-auth", + Enabled: true, + Match: []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Path{Path: &sentinelv1.PathMatch{ + Path: &sentinelv1.StringMatch{Match: &sentinelv1.StringMatch_Prefix{Prefix: "/api"}}, + }}}, + }, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceId: s.KeySpaceID}, + }, + }, + }, + } + + result, err := h.engine.Evaluate(ctx, sess, req, mw) + require.NoError(t, err) + assert.Nil(t, result.Principal) +} diff --git a/svc/sentinel/engine/keyauth.go b/svc/sentinel/engine/keyauth.go new file mode 100644 index 0000000000..cdcce673f8 --- /dev/null +++ b/svc/sentinel/engine/keyauth.go @@ -0,0 +1,169 @@ +package engine + +import ( + "context" + "net/http" + "time" + + sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" + "github.com/unkeyed/unkey/internal/services/keys" + "github.com/unkeyed/unkey/pkg/clock" + "github.com/unkeyed/unkey/pkg/codes" + "github.com/unkeyed/unkey/pkg/fault" + "github.com/unkeyed/unkey/pkg/hash" + "github.com/unkeyed/unkey/pkg/rbac" + "github.com/unkeyed/unkey/pkg/zen" +) + +// KeyAuthExecutor handles KeyAuth policy evaluation by wrapping the existing KeyService. +type KeyAuthExecutor struct { + keyService keys.KeyService + clock clock.Clock +} + +// Execute evaluates a KeyAuth policy against the incoming request. +// It extracts the API key, verifies it using KeyService, and returns a Principal on success. +func (e *KeyAuthExecutor) Execute( + ctx context.Context, + sess *zen.Session, + req *http.Request, + cfg *sentinelv1.KeyAuth, +) (*sentinelv1.Principal, error) { + rawKey := extractKey(req, cfg.GetLocations()) + if rawKey == "" { + if cfg.GetAllowAnonymous() { + return nil, nil + } + + return nil, fault.New("missing API key", + fault.Code(codes.Sentinel.Auth.MissingCredentials.URN()), + fault.Internal("no API key found in request"), + fault.Public("Authentication required. Please provide a valid API key."), + ) + } + + keyHash := hash.Sha256(rawKey) + verifier, logFn, err := e.keyService.Get(ctx, sess, keyHash) + defer logFn() + if err != nil { + return nil, fault.Wrap(err, + fault.Code(codes.Sentinel.Auth.InvalidKey.URN()), + fault.Internal("key lookup failed"), + fault.Public("Authentication failed. The provided API key is invalid."), + ) + } + + // Check basic validation (not found, disabled, expired, workspace disabled, etc.) + if verifier.Status != keys.StatusValid { + return nil, fault.New("invalid API key", + fault.Code(codes.Sentinel.Auth.InvalidKey.URN()), + fault.Internal("key status: "+string(verifier.Status)), + fault.Public("Authentication failed. The provided API key is invalid."), + ) + } + + // Verify the key belongs to the expected key space + if cfg.GetKeySpaceId() != "" && verifier.Key.KeyAuthID != cfg.GetKeySpaceId() { + return nil, fault.New("key does not belong to expected key space", + fault.Code(codes.Sentinel.Auth.InvalidKey.URN()), + fault.Internal("key belongs to key space "+verifier.Key.KeyAuthID+", expected "+cfg.GetKeySpaceId()), + fault.Public("Authentication failed. The provided API key is invalid."), + ) + } + + // Build verify options + var verifyOpts []keys.VerifyOption + + if pq := cfg.GetPermissionQuery(); pq != "" { + query, parseErr := rbac.ParseQuery(pq) + if parseErr != nil { + return nil, fault.Wrap(parseErr, + fault.Code(codes.Sentinel.Internal.InvalidConfiguration.URN()), + fault.Internal("invalid permission query: "+pq), + fault.Public("Service configuration error."), + ) + } + + verifyOpts = append(verifyOpts, keys.WithPermissions(query)) + } + + // Deduct 1 credit per request by default + verifyOpts = append(verifyOpts, keys.WithCredits(1)) + + verifyErr := verifier.Verify(ctx, verifyOpts...) + if verifyErr != nil { + return nil, fault.Wrap(verifyErr, + fault.Code(codes.Sentinel.Internal.InternalServerError.URN()), + fault.Internal("verification error"), + fault.Public("An internal error occurred during authentication."), + ) + } + + // Check post-verification status + switch verifier.Status { + case keys.StatusValid: + // OK + case keys.StatusInsufficientPermissions: + return nil, fault.New("insufficient permissions", + fault.Code(codes.Sentinel.Auth.InsufficientPermissions.URN()), + fault.Internal("key lacks required permissions"), + fault.Public("Access denied. The API key does not have the required permissions."), + ) + case keys.StatusRateLimited: + return nil, fault.New("rate limited", + fault.Code(codes.Sentinel.Auth.RateLimited.URN()), + fault.Internal("auto-applied rate limit exceeded"), + fault.Public("Rate limit exceeded. Please try again later."), + ) + case keys.StatusUsageExceeded: + return nil, fault.New("usage exceeded", + fault.Code(codes.Sentinel.Auth.RateLimited.URN()), + fault.Internal("usage limit exceeded"), + fault.Public("Usage limit exceeded. Please try again later."), + ) + case keys.StatusNotFound, keys.StatusDisabled, keys.StatusExpired, + keys.StatusForbidden, keys.StatusWorkspaceDisabled, keys.StatusWorkspaceNotFound: + // These should have been caught by the pre-verify status check above, + // but handle them here for exhaustiveness. + return nil, fault.New("key verification failed", + fault.Code(codes.Sentinel.Auth.InvalidKey.URN()), + fault.Internal("post-verification status: "+string(verifier.Status)), + fault.Public("Authentication failed."), + ) + } + + // Build the principal + subject := verifier.Key.ID + if verifier.Key.ExternalID.Valid && verifier.Key.ExternalID.String != "" { + subject = verifier.Key.ExternalID.String + } + + claims := map[string]string{ + "key_id": verifier.Key.ID, + "key_space_id": verifier.Key.KeyAuthID, + "api_id": verifier.Key.ApiID, + "workspace_id": verifier.Key.WorkspaceID, + } + if verifier.Key.Name.Valid && verifier.Key.Name.String != "" { + claims["name"] = verifier.Key.Name.String + } + if verifier.Key.IdentityID.Valid && verifier.Key.IdentityID.String != "" { + claims["identity_id"] = verifier.Key.IdentityID.String + } + if verifier.Key.ExternalID.Valid && verifier.Key.ExternalID.String != "" { + claims["external_id"] = verifier.Key.ExternalID.String + } + if verifier.Key.Meta.Valid && verifier.Key.Meta.String != "" { + claims["meta"] = verifier.Key.Meta.String + } + if verifier.Key.Expires.Valid { + claims["expires"] = verifier.Key.Expires.Time.Format(time.RFC3339) + } + + //nolint:exhaustruct + return &sentinelv1.Principal{ + Subject: subject, + Type: sentinelv1.PrincipalType_PRINCIPAL_TYPE_API_KEY, + Claims: claims, + }, nil +} diff --git a/svc/sentinel/engine/keyextract.go b/svc/sentinel/engine/keyextract.go new file mode 100644 index 0000000000..abb90be538 --- /dev/null +++ b/svc/sentinel/engine/keyextract.go @@ -0,0 +1,72 @@ +package engine + +import ( + "net/http" + "strings" + + sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" +) + +// extractKey tries each location in order and returns the first non-empty key. +// If no locations are configured, it defaults to extracting a Bearer token from +// the Authorization header. +func extractKey(req *http.Request, locations []*sentinelv1.KeyLocation) string { + if len(locations) == 0 { + return extractBearer(req) + } + + for _, loc := range locations { + var key string + switch l := loc.GetLocation().(type) { + case *sentinelv1.KeyLocation_Bearer: + key = extractBearer(req) + case *sentinelv1.KeyLocation_Header: + key = extractHeader(req, l.Header) + case *sentinelv1.KeyLocation_QueryParam: + key = extractQueryParam(req, l.QueryParam) + } + if key != "" { + return key + } + } + return "" +} + +// extractBearer extracts the token from "Authorization: Bearer ". +func extractBearer(req *http.Request) string { + auth := req.Header.Get("Authorization") + if auth == "" { + return "" + } + const prefix = "Bearer " + if len(auth) > len(prefix) && strings.EqualFold(auth[:len(prefix)], prefix) { + return auth[len(prefix):] + } + return "" +} + +// extractHeader extracts the key from a named header, optionally stripping a prefix. +func extractHeader(req *http.Request, loc *sentinelv1.HeaderKeyLocation) string { + if loc == nil { + return "" + } + val := req.Header.Get(loc.GetName()) + if val == "" { + return "" + } + if sp := loc.GetStripPrefix(); sp != "" { + if len(val) > len(sp) && strings.EqualFold(val[:len(sp)], sp) { + return val[len(sp):] + } + return "" + } + return val +} + +// extractQueryParam extracts the key from a URL query parameter. +func extractQueryParam(req *http.Request, loc *sentinelv1.QueryParamKeyLocation) string { + if loc == nil { + return "" + } + return req.URL.Query().Get(loc.GetName()) +} diff --git a/svc/sentinel/engine/keyextract_test.go b/svc/sentinel/engine/keyextract_test.go new file mode 100644 index 0000000000..c9b994f637 --- /dev/null +++ b/svc/sentinel/engine/keyextract_test.go @@ -0,0 +1,175 @@ +package engine + +import ( + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" +) + +func TestExtractKey_DefaultBearer(t *testing.T) { + t.Parallel() + + req := &http.Request{Header: http.Header{}} + req.Header.Set("Authorization", "Bearer sk_live_abc123") + + key := extractKey(req, nil) + assert.Equal(t, "sk_live_abc123", key) +} + +func TestExtractKey_BearerCaseInsensitive(t *testing.T) { + t.Parallel() + + req := &http.Request{Header: http.Header{}} + req.Header.Set("Authorization", "bearer sk_live_abc123") + + key := extractBearer(req) + assert.Equal(t, "sk_live_abc123", key) +} + +func TestExtractKey_NoAuthHeader(t *testing.T) { + t.Parallel() + + req := &http.Request{Header: http.Header{}} + key := extractKey(req, nil) + assert.Empty(t, key) +} + +func TestExtractKey_BearerLocation(t *testing.T) { + t.Parallel() + + req := &http.Request{Header: http.Header{}} + req.Header.Set("Authorization", "Bearer my_key") + + //nolint:exhaustruct + locations := []*sentinelv1.KeyLocation{ + {Location: &sentinelv1.KeyLocation_Bearer{Bearer: &sentinelv1.BearerTokenLocation{}}}, + } + + key := extractKey(req, locations) + assert.Equal(t, "my_key", key) +} + +func TestExtractKey_HeaderLocation(t *testing.T) { + t.Parallel() + + req := &http.Request{Header: http.Header{}} + req.Header.Set("X-API-Key", "custom_key_123") + + //nolint:exhaustruct + locations := []*sentinelv1.KeyLocation{ + {Location: &sentinelv1.KeyLocation_Header{ + Header: &sentinelv1.HeaderKeyLocation{Name: "X-API-Key"}, + }}, + } + + key := extractKey(req, locations) + assert.Equal(t, "custom_key_123", key) +} + +func TestExtractKey_HeaderWithStripPrefix(t *testing.T) { + t.Parallel() + + req := &http.Request{Header: http.Header{}} + req.Header.Set("Authorization", "ApiKey sk_live_abc123") + + //nolint:exhaustruct + locations := []*sentinelv1.KeyLocation{ + {Location: &sentinelv1.KeyLocation_Header{ + Header: &sentinelv1.HeaderKeyLocation{ + Name: "Authorization", + StripPrefix: "ApiKey ", + }, + }}, + } + + key := extractKey(req, locations) + assert.Equal(t, "sk_live_abc123", key) +} + +func TestExtractKey_HeaderStripPrefixMismatch(t *testing.T) { + t.Parallel() + + req := &http.Request{Header: http.Header{}} + req.Header.Set("Authorization", "Bearer sk_live_abc123") + + //nolint:exhaustruct + locations := []*sentinelv1.KeyLocation{ + {Location: &sentinelv1.KeyLocation_Header{ + Header: &sentinelv1.HeaderKeyLocation{ + Name: "Authorization", + StripPrefix: "ApiKey ", + }, + }}, + } + + key := extractKey(req, locations) + assert.Empty(t, key) +} + +func TestExtractKey_QueryParam(t *testing.T) { + t.Parallel() + + req := &http.Request{ + Header: http.Header{}, + URL: &url.URL{RawQuery: "api_key=query_key_123"}, + } + + //nolint:exhaustruct + locations := []*sentinelv1.KeyLocation{ + {Location: &sentinelv1.KeyLocation_QueryParam{ + QueryParam: &sentinelv1.QueryParamKeyLocation{Name: "api_key"}, + }}, + } + + key := extractKey(req, locations) + assert.Equal(t, "query_key_123", key) +} + +func TestExtractKey_FallbackOrder(t *testing.T) { + t.Parallel() + + // First location has nothing, second has the key + req := &http.Request{ + Header: http.Header{}, + URL: &url.URL{RawQuery: "token=fallback_key"}, + } + + //nolint:exhaustruct + locations := []*sentinelv1.KeyLocation{ + {Location: &sentinelv1.KeyLocation_Header{ + Header: &sentinelv1.HeaderKeyLocation{Name: "X-API-Key"}, + }}, + {Location: &sentinelv1.KeyLocation_QueryParam{ + QueryParam: &sentinelv1.QueryParamKeyLocation{Name: "token"}, + }}, + } + + key := extractKey(req, locations) + assert.Equal(t, "fallback_key", key) +} + +func TestExtractKey_FirstLocationWins(t *testing.T) { + t.Parallel() + + req := &http.Request{ + Header: http.Header{}, + URL: &url.URL{RawQuery: "token=query_key"}, + } + req.Header.Set("X-API-Key", "header_key") + + //nolint:exhaustruct + locations := []*sentinelv1.KeyLocation{ + {Location: &sentinelv1.KeyLocation_Header{ + Header: &sentinelv1.HeaderKeyLocation{Name: "X-API-Key"}, + }}, + {Location: &sentinelv1.KeyLocation_QueryParam{ + QueryParam: &sentinelv1.QueryParamKeyLocation{Name: "token"}, + }}, + } + + key := extractKey(req, locations) + assert.Equal(t, "header_key", key) +} diff --git a/svc/sentinel/engine/match.go b/svc/sentinel/engine/match.go new file mode 100644 index 0000000000..a1111f050b --- /dev/null +++ b/svc/sentinel/engine/match.go @@ -0,0 +1,178 @@ +package engine + +import ( + "fmt" + "net/http" + "regexp" + "strings" + "sync" + + sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" +) + +// regexCache caches compiled regular expressions to avoid recompilation. +type regexCache struct { + mu sync.RWMutex + cache map[string]*regexp.Regexp +} + +func newRegexCache() *regexCache { + //nolint:exhaustruct + return ®exCache{ + cache: make(map[string]*regexp.Regexp), + } +} + +func (rc *regexCache) get(pattern string) (*regexp.Regexp, error) { + rc.mu.RLock() + re, ok := rc.cache[pattern] + rc.mu.RUnlock() + if ok { + return re, nil + } + + re, err := regexp.Compile(pattern) + if err != nil { + return nil, fmt.Errorf("invalid regex %q: %w", pattern, err) + } + + rc.mu.Lock() + rc.cache[pattern] = re + rc.mu.Unlock() + return re, nil +} + +// matchesRequest evaluates all match expressions against the request. +// All expressions must match (AND semantics). An empty list matches all requests. +func matchesRequest(req *http.Request, exprs []*sentinelv1.MatchExpr, rc *regexCache) (bool, error) { + for _, expr := range exprs { + matched, err := evalMatchExpr(req, expr, rc) + if err != nil { + return false, err + } + if !matched { + return false, nil + } + } + return true, nil +} + +func evalMatchExpr(req *http.Request, expr *sentinelv1.MatchExpr, rc *regexCache) (bool, error) { + switch e := expr.GetExpr().(type) { + case *sentinelv1.MatchExpr_Path: + return evalPathMatch(req, e.Path, rc) + case *sentinelv1.MatchExpr_Method: + return evalMethodMatch(req, e.Method), nil + case *sentinelv1.MatchExpr_Header: + return evalHeaderMatch(req, e.Header, rc) + case *sentinelv1.MatchExpr_QueryParam: + return evalQueryParamMatch(req, e.QueryParam, rc) + default: + return false, nil + } +} + +func evalPathMatch(req *http.Request, pm *sentinelv1.PathMatch, rc *regexCache) (bool, error) { + if pm == nil || pm.GetPath() == nil { + return true, nil + } + return evalStringMatch(req.URL.Path, pm.GetPath(), rc) +} + +// evalMethodMatch checks if the request method matches any of the specified methods. +// Always case-insensitive per HTTP spec. OR semantics across the list. +func evalMethodMatch(req *http.Request, mm *sentinelv1.MethodMatch) bool { + if mm == nil || len(mm.GetMethods()) == 0 { + return true + } + for _, m := range mm.GetMethods() { + if strings.EqualFold(req.Method, m) { + return true + } + } + return false +} + +func evalHeaderMatch(req *http.Request, hm *sentinelv1.HeaderMatch, rc *regexCache) (bool, error) { + if hm == nil { + return true, nil + } + values := req.Header.Values(hm.GetName()) + switch m := hm.GetMatch().(type) { + case *sentinelv1.HeaderMatch_Present: + return m.Present == (len(values) > 0), nil + case *sentinelv1.HeaderMatch_Value: + for _, v := range values { + matched, err := evalStringMatch(v, m.Value, rc) + if err != nil { + return false, err + } + if matched { + return true, nil + } + } + return false, nil + default: + // No match specified, just check presence + return len(values) > 0, nil + } +} + +func evalQueryParamMatch(req *http.Request, qm *sentinelv1.QueryParamMatch, rc *regexCache) (bool, error) { + if qm == nil { + return true, nil + } + values, exists := req.URL.Query()[qm.GetName()] + switch m := qm.GetMatch().(type) { + case *sentinelv1.QueryParamMatch_Present: + return m.Present == exists, nil + case *sentinelv1.QueryParamMatch_Value: + for _, v := range values { + matched, err := evalStringMatch(v, m.Value, rc) + if err != nil { + return false, err + } + if matched { + return true, nil + } + } + return false, nil + default: + return exists, nil + } +} + +// evalStringMatch evaluates a value against a StringMatch (exact, prefix, or regex). +func evalStringMatch(value string, sm *sentinelv1.StringMatch, rc *regexCache) (bool, error) { + if sm == nil { + return true, nil + } + + switch m := sm.GetMatch().(type) { + case *sentinelv1.StringMatch_Exact: + if sm.GetIgnoreCase() { + return strings.EqualFold(value, m.Exact), nil + } + return value == m.Exact, nil + + case *sentinelv1.StringMatch_Prefix: + if sm.GetIgnoreCase() { + return len(value) >= len(m.Prefix) && strings.EqualFold(value[:len(m.Prefix)], m.Prefix), nil + } + return strings.HasPrefix(value, m.Prefix), nil + + case *sentinelv1.StringMatch_Regex: + pattern := m.Regex + if sm.GetIgnoreCase() { + pattern = "(?i)" + pattern + } + re, err := rc.get(pattern) + if err != nil { + return false, err + } + return re.MatchString(value), nil + + default: + return true, nil + } +} diff --git a/svc/sentinel/engine/match_test.go b/svc/sentinel/engine/match_test.go new file mode 100644 index 0000000000..62ca6ea105 --- /dev/null +++ b/svc/sentinel/engine/match_test.go @@ -0,0 +1,311 @@ +package engine + +import ( + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" +) + +func TestMatchesRequest_EmptyList(t *testing.T) { + t.Parallel() + req := &http.Request{Method: "GET", URL: &url.URL{Path: "/api"}, Header: http.Header{}} + matched, err := matchesRequest(req, nil, newRegexCache()) + require.NoError(t, err) + assert.True(t, matched) +} + +func TestMatchesRequest_PathExact(t *testing.T) { + t.Parallel() + rc := newRegexCache() + req := &http.Request{Method: "GET", URL: &url.URL{Path: "/api/v1"}, Header: http.Header{}} + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Path{Path: &sentinelv1.PathMatch{ + Path: &sentinelv1.StringMatch{Match: &sentinelv1.StringMatch_Exact{Exact: "/api/v1"}}, + }}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.True(t, matched) +} + +func TestMatchesRequest_PathExactMismatch(t *testing.T) { + t.Parallel() + rc := newRegexCache() + req := &http.Request{Method: "GET", URL: &url.URL{Path: "/api/v2"}, Header: http.Header{}} + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Path{Path: &sentinelv1.PathMatch{ + Path: &sentinelv1.StringMatch{Match: &sentinelv1.StringMatch_Exact{Exact: "/api/v1"}}, + }}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.False(t, matched) +} + +func TestMatchesRequest_PathPrefix(t *testing.T) { + t.Parallel() + rc := newRegexCache() + req := &http.Request{Method: "GET", URL: &url.URL{Path: "/api/v1/users"}, Header: http.Header{}} + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Path{Path: &sentinelv1.PathMatch{ + Path: &sentinelv1.StringMatch{Match: &sentinelv1.StringMatch_Prefix{Prefix: "/api/v1"}}, + }}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.True(t, matched) +} + +func TestMatchesRequest_PathRegex(t *testing.T) { + t.Parallel() + rc := newRegexCache() + req := &http.Request{Method: "GET", URL: &url.URL{Path: "/api/v2/users/123"}, Header: http.Header{}} + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Path{Path: &sentinelv1.PathMatch{ + Path: &sentinelv1.StringMatch{Match: &sentinelv1.StringMatch_Regex{Regex: `^/api/v\d+/users/\d+$`}}, + }}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.True(t, matched) +} + +func TestMatchesRequest_PathCaseInsensitive(t *testing.T) { + t.Parallel() + rc := newRegexCache() + req := &http.Request{Method: "GET", URL: &url.URL{Path: "/API/V1"}, Header: http.Header{}} + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Path{Path: &sentinelv1.PathMatch{ + Path: &sentinelv1.StringMatch{ + IgnoreCase: true, + Match: &sentinelv1.StringMatch_Exact{Exact: "/api/v1"}, + }, + }}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.True(t, matched) +} + +func TestMatchesRequest_MethodMatch(t *testing.T) { + t.Parallel() + rc := newRegexCache() + + tests := []struct { + name string + method string + methods []string + expected bool + }{ + {"exact match", "GET", []string{"GET"}, true}, + {"case insensitive", "get", []string{"GET"}, true}, + {"multiple methods", "POST", []string{"GET", "POST"}, true}, + {"no match", "DELETE", []string{"GET", "POST"}, false}, + {"empty methods matches all", "DELETE", nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + req := &http.Request{Method: tt.method, URL: &url.URL{Path: "/"}, Header: http.Header{}} + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Method{Method: &sentinelv1.MethodMatch{Methods: tt.methods}}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.Equal(t, tt.expected, matched) + }) + } +} + +func TestMatchesRequest_HeaderPresent(t *testing.T) { + t.Parallel() + rc := newRegexCache() + + req := &http.Request{Method: "GET", URL: &url.URL{Path: "/"}, Header: http.Header{}} + req.Header.Set("Authorization", "Bearer token") + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Header{Header: &sentinelv1.HeaderMatch{ + Name: "Authorization", + Match: &sentinelv1.HeaderMatch_Present{Present: true}, + }}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.True(t, matched) +} + +func TestMatchesRequest_HeaderNotPresent(t *testing.T) { + t.Parallel() + rc := newRegexCache() + + req := &http.Request{Method: "GET", URL: &url.URL{Path: "/"}, Header: http.Header{}} + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Header{Header: &sentinelv1.HeaderMatch{ + Name: "Authorization", + Match: &sentinelv1.HeaderMatch_Present{Present: true}, + }}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.False(t, matched) +} + +func TestMatchesRequest_HeaderValue(t *testing.T) { + t.Parallel() + rc := newRegexCache() + + req := &http.Request{Method: "GET", URL: &url.URL{Path: "/"}, Header: http.Header{}} + req.Header.Set("Content-Type", "application/json") + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Header{Header: &sentinelv1.HeaderMatch{ + Name: "Content-Type", + Match: &sentinelv1.HeaderMatch_Value{Value: &sentinelv1.StringMatch{ + Match: &sentinelv1.StringMatch_Exact{Exact: "application/json"}, + }}, + }}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.True(t, matched) +} + +func TestMatchesRequest_QueryParamPresent(t *testing.T) { + t.Parallel() + rc := newRegexCache() + + req := &http.Request{ + Method: "GET", + URL: &url.URL{Path: "/", RawQuery: "debug=true"}, + Header: http.Header{}, + } + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_QueryParam{QueryParam: &sentinelv1.QueryParamMatch{ + Name: "debug", + Match: &sentinelv1.QueryParamMatch_Present{Present: true}, + }}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.True(t, matched) +} + +func TestMatchesRequest_QueryParamValue(t *testing.T) { + t.Parallel() + rc := newRegexCache() + + req := &http.Request{ + Method: "GET", + URL: &url.URL{Path: "/", RawQuery: "version=v2"}, + Header: http.Header{}, + } + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_QueryParam{QueryParam: &sentinelv1.QueryParamMatch{ + Name: "version", + Match: &sentinelv1.QueryParamMatch_Value{Value: &sentinelv1.StringMatch{ + Match: &sentinelv1.StringMatch_Prefix{Prefix: "v"}, + }}, + }}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.True(t, matched) +} + +func TestMatchesRequest_ANDSemantics(t *testing.T) { + t.Parallel() + rc := newRegexCache() + + // Path matches but method doesn't + req := &http.Request{Method: "DELETE", URL: &url.URL{Path: "/api/v1"}, Header: http.Header{}} + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Path{Path: &sentinelv1.PathMatch{ + Path: &sentinelv1.StringMatch{Match: &sentinelv1.StringMatch_Prefix{Prefix: "/api"}}, + }}}, + {Expr: &sentinelv1.MatchExpr_Method{Method: &sentinelv1.MethodMatch{Methods: []string{"GET", "POST"}}}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.False(t, matched) +} + +func TestMatchesRequest_ANDSemanticsAllMatch(t *testing.T) { + t.Parallel() + rc := newRegexCache() + + req := &http.Request{Method: "POST", URL: &url.URL{Path: "/api/v1"}, Header: http.Header{}} + + //nolint:exhaustruct + exprs := []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Path{Path: &sentinelv1.PathMatch{ + Path: &sentinelv1.StringMatch{Match: &sentinelv1.StringMatch_Prefix{Prefix: "/api"}}, + }}}, + {Expr: &sentinelv1.MatchExpr_Method{Method: &sentinelv1.MethodMatch{Methods: []string{"GET", "POST"}}}}, + } + + matched, err := matchesRequest(req, exprs, rc) + require.NoError(t, err) + assert.True(t, matched) +} + +func TestRegexCache(t *testing.T) { + t.Parallel() + rc := newRegexCache() + + re1, err := rc.get(`^/api/v\d+$`) + require.NoError(t, err) + assert.True(t, re1.MatchString("/api/v1")) + + // Second call should return cached regex + re2, err := rc.get(`^/api/v\d+$`) + require.NoError(t, err) + assert.Equal(t, re1, re2) +} + +func TestRegexCache_InvalidPattern(t *testing.T) { + t.Parallel() + rc := newRegexCache() + + _, err := rc.get(`[invalid`) + assert.Error(t, err) +} diff --git a/svc/sentinel/middleware/error_handling.go b/svc/sentinel/middleware/error_handling.go index c59d200dfd..0faa7a8e63 100644 --- a/svc/sentinel/middleware/error_handling.go +++ b/svc/sentinel/middleware/error_handling.go @@ -23,6 +23,13 @@ func WithProxyErrorHandling() zen.Middleware { return nil } + // If the error already has a fault code (e.g. from the engine's + // auth/rate-limit checks), it's not a proxy error — pass it through + // so the observability middleware maps it to the correct status. + if _, hasCode := fault.GetCode(err); hasCode { + return err + } + tracking, ok := handler.SentinelTrackingFromContext(ctx) if !ok { return err diff --git a/svc/sentinel/middleware/observability.go b/svc/sentinel/middleware/observability.go index 500edd47a7..3f8d192bdb 100644 --- a/svc/sentinel/middleware/observability.go +++ b/svc/sentinel/middleware/observability.go @@ -111,6 +111,28 @@ func getErrorPageInfo(urn codes.URN) errorPageInfo { Message: "The sentinel is misconfigured. Please contact support.", } + // Sentinel Auth Errors + case codes.Sentinel.Auth.MissingCredentials.URN(): + return errorPageInfo{ + Status: http.StatusUnauthorized, + Message: "Authentication required. Please provide valid credentials.", + } + case codes.Sentinel.Auth.InvalidKey.URN(): + return errorPageInfo{ + Status: http.StatusUnauthorized, + Message: "The provided API key is invalid, disabled, or expired.", + } + case codes.Sentinel.Auth.InsufficientPermissions.URN(): + return errorPageInfo{ + Status: http.StatusForbidden, + Message: "The API key does not have the required permissions.", + } + case codes.Sentinel.Auth.RateLimited.URN(): + return errorPageInfo{ + Status: http.StatusTooManyRequests, + Message: "Rate limit exceeded. Please try again later.", + } + // User/Client Errors case codes.User.BadRequest.ClientClosedRequest.URN(): return errorPageInfo{ @@ -156,6 +178,12 @@ func categorizeErrorType(urn codes.URN, statusCode int, hasError bool) string { case codes.User.BadRequest.ClientClosedRequest.URN(), codes.User.BadRequest.MissingRequiredHeader.URN(): return "user" + + case codes.Sentinel.Auth.MissingCredentials.URN(), + codes.Sentinel.Auth.InvalidKey.URN(), + codes.Sentinel.Auth.InsufficientPermissions.URN(), + codes.Sentinel.Auth.RateLimited.URN(): + return "user" } if statusCode >= 500 { diff --git a/svc/sentinel/routes/BUILD.bazel b/svc/sentinel/routes/BUILD.bazel index 420f22c0db..dd338e04d0 100644 --- a/svc/sentinel/routes/BUILD.bazel +++ b/svc/sentinel/routes/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "//pkg/clickhouse", "//pkg/clock", "//pkg/zen", + "//svc/sentinel/engine", "//svc/sentinel/middleware", "//svc/sentinel/routes/internal_health", "//svc/sentinel/routes/proxy", diff --git a/svc/sentinel/routes/proxy/BUILD.bazel b/svc/sentinel/routes/proxy/BUILD.bazel index 16f24a470f..bc436a00fc 100644 --- a/svc/sentinel/routes/proxy/BUILD.bazel +++ b/svc/sentinel/routes/proxy/BUILD.bazel @@ -16,6 +16,7 @@ go_library( "//pkg/timing", "//pkg/uid", "//pkg/zen", + "//svc/sentinel/engine", "//svc/sentinel/services/router", ], ) diff --git a/svc/sentinel/routes/proxy/handler.go b/svc/sentinel/routes/proxy/handler.go index 79971594fb..276ccafe5b 100644 --- a/svc/sentinel/routes/proxy/handler.go +++ b/svc/sentinel/routes/proxy/handler.go @@ -16,6 +16,7 @@ import ( "github.com/unkeyed/unkey/pkg/timing" "github.com/unkeyed/unkey/pkg/uid" "github.com/unkeyed/unkey/pkg/zen" + "github.com/unkeyed/unkey/svc/sentinel/engine" "github.com/unkeyed/unkey/svc/sentinel/services/router" ) @@ -26,6 +27,7 @@ type Handler struct { SentinelID string Region string MaxRequestBodySize int64 + Engine engine.Evaluator } func (h *Handler) Method() string { @@ -65,6 +67,27 @@ func (h *Handler) Handle(ctx context.Context, sess *zen.Session) error { return err } + // Always strip incoming X-Unkey-Principal header to prevent spoofing + req.Header.Del(engine.PrincipalHeader) + + // Evaluate sentinel middleware policies + mw := engine.ParseMiddleware(deployment.SentinelConfig) + if mw != nil && h.Engine != nil { + result, evalErr := h.Engine.Evaluate(ctx, sess, req, mw) + if evalErr != nil { + return evalErr + } + + if result.Principal != nil { + principalJSON, serErr := engine.SerializePrincipal(result.Principal) + if serErr != nil { + logger.Error("failed to serialize principal", "error", serErr) + } else { + req.Header.Set(engine.PrincipalHeader, principalJSON) + } + } + } + var requestBody []byte if req.Body != nil { requestBody, err = io.ReadAll(req.Body) diff --git a/svc/sentinel/routes/register.go b/svc/sentinel/routes/register.go index e95c0ed5b5..09a33f633b 100644 --- a/svc/sentinel/routes/register.go +++ b/svc/sentinel/routes/register.go @@ -16,7 +16,7 @@ func Register(srv *zen.Server, svc *Services) { withObservability := middleware.WithObservability(svc.EnvironmentID, svc.Region) withSentinelLogging := middleware.WithSentinelLogging(svc.ClickHouse, svc.Clock, svc.SentinelID, svc.Region) withProxyErrorHandling := middleware.WithProxyErrorHandling() - withLogging := zen.WithLogging() + withLogging := zen.WithLogging(zen.SkipPaths("/_unkey/internal/", "/health/")) defaultMiddlewares := []zen.Middleware{ withPanicRecovery, withObservability, @@ -50,6 +50,7 @@ func Register(srv *zen.Server, svc *Services) { SentinelID: svc.SentinelID, Region: svc.Region, MaxRequestBodySize: svc.MaxRequestBodySize, + Engine: svc.Engine, }, ) } diff --git a/svc/sentinel/routes/services.go b/svc/sentinel/routes/services.go index f4fd40d633..63ed3a0ed7 100644 --- a/svc/sentinel/routes/services.go +++ b/svc/sentinel/routes/services.go @@ -3,6 +3,7 @@ package routes import ( "github.com/unkeyed/unkey/pkg/clickhouse" "github.com/unkeyed/unkey/pkg/clock" + "github.com/unkeyed/unkey/svc/sentinel/engine" "github.com/unkeyed/unkey/svc/sentinel/services/router" ) @@ -16,4 +17,5 @@ type Services struct { Region string ClickHouse clickhouse.ClickHouse MaxRequestBodySize int64 + Engine engine.Evaluator } diff --git a/svc/sentinel/run.go b/svc/sentinel/run.go index 235034758d..ab133c3716 100644 --- a/svc/sentinel/run.go +++ b/svc/sentinel/run.go @@ -6,18 +6,26 @@ import ( "fmt" "log/slog" "net" + "time" + "github.com/unkeyed/unkey/internal/services/keys" + "github.com/unkeyed/unkey/internal/services/ratelimit" + "github.com/unkeyed/unkey/internal/services/usagelimiter" + "github.com/unkeyed/unkey/pkg/cache" "github.com/unkeyed/unkey/pkg/cache/clustering" "github.com/unkeyed/unkey/pkg/clickhouse" "github.com/unkeyed/unkey/pkg/clock" "github.com/unkeyed/unkey/pkg/cluster" + "github.com/unkeyed/unkey/pkg/counter" "github.com/unkeyed/unkey/pkg/db" "github.com/unkeyed/unkey/pkg/logger" "github.com/unkeyed/unkey/pkg/otel" "github.com/unkeyed/unkey/pkg/prometheus" + "github.com/unkeyed/unkey/pkg/rbac" "github.com/unkeyed/unkey/pkg/runner" "github.com/unkeyed/unkey/pkg/version" "github.com/unkeyed/unkey/pkg/zen" + "github.com/unkeyed/unkey/svc/sentinel/engine" "github.com/unkeyed/unkey/svc/sentinel/routes" "github.com/unkeyed/unkey/svc/sentinel/services/router" ) @@ -159,6 +167,11 @@ func Run(ctx context.Context, cfg Config) error { } r.Defer(routerSvc.Close) + // Initialize middleware engine for KeyAuth and other sentinel policies. + // If Redis is unavailable, sentinel continues without middleware evaluation + // (deployments are proxied as pass-through). + middlewareEngine := initMiddlewareEngine(cfg, database, ch, clk, r) + svcs := &routes.Services{ RouterService: routerSvc, Clock: clk, @@ -168,6 +181,7 @@ func Run(ctx context.Context, cfg Config) error { Region: cfg.Region, ClickHouse: ch, MaxRequestBodySize: maxRequestBodySize, + Engine: middlewareEngine, } srv, err := zen.New(zen.Config{ @@ -206,3 +220,75 @@ func Run(ctx context.Context, cfg Config) error { logger.Info("Sentinel server shut down successfully") return nil } + +// initMiddlewareEngine creates the middleware engine backed by Redis. +// Returns nil (pass-through mode) when Redis URL is empty or connection fails. +func initMiddlewareEngine(cfg Config, database db.Database, ch clickhouse.ClickHouse, clk clock.Clock, r *runner.Runner) engine.Evaluator { + if cfg.Redis.URL == "" { + logger.Info("redis URL not configured, middleware engine disabled") + return nil + } + + redisCounter, err := counter.NewRedis(counter.RedisConfig{ + RedisURL: cfg.Redis.URL, + }) + if err != nil { + logger.Error("failed to connect to redis, middleware engine disabled", "error", err) + return nil + } + r.Defer(redisCounter.Close) + + rateLimiter, err := ratelimit.New(ratelimit.Config{ + Clock: clk, + Counter: redisCounter, + }) + if err != nil { + logger.Error("failed to create rate limiter, middleware engine disabled", "error", err) + return nil + } + r.Defer(rateLimiter.Close) + + usageLimiter, err := usagelimiter.NewCounter(usagelimiter.CounterConfig{ + DB: database, + Counter: redisCounter, + TTL: 60 * time.Second, + ReplayWorkers: 8, + }) + if err != nil { + logger.Error("failed to create usage limiter, middleware engine disabled", "error", err) + return nil + } + r.Defer(usageLimiter.Close) + + keyCache, err := cache.New[string, db.CachedKeyData](cache.Config[string, db.CachedKeyData]{ + Fresh: 10 * time.Second, + Stale: 10 * time.Minute, + MaxSize: 100_000, + Resource: "sentinel_key_cache", + Clock: clk, + }) + if err != nil { + logger.Error("failed to create key cache, middleware engine disabled", "error", err) + return nil + } + + keyService, err := keys.New(keys.Config{ + DB: database, + RateLimiter: rateLimiter, + RBAC: rbac.New(), + Clickhouse: ch, + Region: cfg.Region, + UsageLimiter: usageLimiter, + KeyCache: keyCache, + }) + if err != nil { + logger.Error("failed to create key service, middleware engine disabled", "error", err) + return nil + } + + logger.Info("middleware engine initialized") + return engine.New(engine.Config{ + KeyService: keyService, + Clock: clk, + }) +} From 1c0f3674f0f03f646889f16c4ecec02d636ba33e Mon Sep 17 00:00:00 2001 From: Flo <53355483+Flo4604@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:02:17 +0100 Subject: [PATCH 2/8] fix error pages (#5083) * fix error pages * remove test * move some files * Update svc/frontline/internal/errorpage/error.go.tmpl Co-authored-by: Andreas Thomas * [autofix.ci] apply automated fixes --------- Co-authored-by: Andreas Thomas Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- dev/Tiltfile | 2 +- svc/frontline/BUILD.bazel | 1 + svc/frontline/internal/errorpage/BUILD.bazel | 13 ++ svc/frontline/internal/errorpage/doc.go | 19 ++ .../internal/errorpage/error.go.tmpl | 169 ++++++++++++++++++ svc/frontline/internal/errorpage/errorpage.go | 32 ++++ svc/frontline/internal/errorpage/interface.go | 27 +++ svc/frontline/middleware/BUILD.bazel | 1 + svc/frontline/middleware/observability.go | 51 +++--- svc/frontline/routes/BUILD.bazel | 1 + svc/frontline/routes/register.go | 4 +- svc/frontline/routes/services.go | 12 +- svc/frontline/run.go | 12 +- svc/frontline/services/proxy/BUILD.bazel | 1 + svc/frontline/services/proxy/forward.go | 107 ++++++++++- svc/frontline/services/proxy/interface.go | 4 + svc/frontline/services/proxy/service.go | 36 ++-- svc/sentinel/middleware/observability.go | 7 + 18 files changed, 438 insertions(+), 61 deletions(-) create mode 100644 svc/frontline/internal/errorpage/BUILD.bazel create mode 100644 svc/frontline/internal/errorpage/doc.go create mode 100644 svc/frontline/internal/errorpage/error.go.tmpl create mode 100644 svc/frontline/internal/errorpage/errorpage.go create mode 100644 svc/frontline/internal/errorpage/interface.go diff --git a/dev/Tiltfile b/dev/Tiltfile index d23b561c54..875629a5c0 100644 --- a/dev/Tiltfile +++ b/dev/Tiltfile @@ -286,7 +286,7 @@ k8s_resource( # Build locally and load into minikube local_resource( 'build-sentinel-image', - 'docker build -t unkey/sentinel:latest -f Dockerfile.tilt .. && minikube image load unkey/sentinel:latest', + 'docker build -t unkey/sentinel:latest -f Dockerfile.tilt .. && minikube image load unkey/sentinel:latest && kubectl rollout restart deployment -n sentinel --selector=app.kubernetes.io/component=sentinel 2>/dev/null || true', deps=['../bin'], resource_deps=['build-unkey'], labels=['build'], diff --git a/svc/frontline/BUILD.bazel b/svc/frontline/BUILD.bazel index 83dc708f48..ce6e620ba0 100644 --- a/svc/frontline/BUILD.bazel +++ b/svc/frontline/BUILD.bazel @@ -27,6 +27,7 @@ go_library( "//pkg/tls", "//pkg/version", "//pkg/zen", + "//svc/frontline/internal/errorpage", "//svc/frontline/routes", "//svc/frontline/services/caches", "//svc/frontline/services/certmanager", diff --git a/svc/frontline/internal/errorpage/BUILD.bazel b/svc/frontline/internal/errorpage/BUILD.bazel new file mode 100644 index 0000000000..f38bdc8ea7 --- /dev/null +++ b/svc/frontline/internal/errorpage/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "errorpage", + srcs = [ + "doc.go", + "errorpage.go", + "interface.go", + ], + embedsrcs = ["error.go.tmpl"], + importpath = "github.com/unkeyed/unkey/svc/frontline/internal/errorpage", + visibility = ["//svc/frontline:__subpackages__"], +) diff --git a/svc/frontline/internal/errorpage/doc.go b/svc/frontline/internal/errorpage/doc.go new file mode 100644 index 0000000000..ca7529b27d --- /dev/null +++ b/svc/frontline/internal/errorpage/doc.go @@ -0,0 +1,19 @@ +// Package errorpage renders HTML error pages for frontline. +// +// Frontline shows error pages for its own errors (routing failures, proxy +// errors) and for sentinel errors (auth rejections, rate limits). The +// [Renderer] interface allows swapping the template, e.g. for custom +// domains with branded error pages. +// +// # Template +// +// The default implementation embeds error.go.tmpl at compile time and +// renders it with [html/template]. The template receives a [Data] struct +// and supports dark/light mode via prefers-color-scheme. +// +// # Content Negotiation +// +// This package only produces HTML. The caller (frontline middleware or +// proxy) is responsible for checking the Accept header and falling back +// to JSON when the client prefers it. +package errorpage diff --git a/svc/frontline/internal/errorpage/error.go.tmpl b/svc/frontline/internal/errorpage/error.go.tmpl new file mode 100644 index 0000000000..9c32567847 --- /dev/null +++ b/svc/frontline/internal/errorpage/error.go.tmpl @@ -0,0 +1,169 @@ + + + + + + {{.StatusCode}} {{.Title}} + + + +
+
+
{{.StatusCode}}
+
{{.Title}}
+
+ +
{{.Message}}
+ +
+ {{if .RequestID}} +
+ Request ID + {{.RequestID}} +
+ {{end}} + {{if .ErrorCode}} +
+ Code + {{if .DocsURL}}{{.ErrorCode}}{{else}}{{.ErrorCode}}{{end}} +
+ {{end}} +
+ + +
+ + diff --git a/svc/frontline/internal/errorpage/errorpage.go b/svc/frontline/internal/errorpage/errorpage.go new file mode 100644 index 0000000000..4076ebb0ae --- /dev/null +++ b/svc/frontline/internal/errorpage/errorpage.go @@ -0,0 +1,32 @@ +package errorpage + +import ( + "bytes" + _ "embed" + "html/template" +) + +//go:embed error.go.tmpl +var defaultTemplate string + +// defaultRenderer uses the embedded HTML template. +type defaultRenderer struct { + tmpl *template.Template +} + +func (r *defaultRenderer) Render(data Data) ([]byte, error) { + var buf bytes.Buffer + if err := r.tmpl.Execute(&buf, data); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// NewRenderer returns a [Renderer] that uses the default embedded error page template. +// Panics if the template fails to parse (should never happen with an embedded template). +func NewRenderer() Renderer { + return &defaultRenderer{ + tmpl: template.Must(template.New("error").Parse(defaultTemplate)), + } +} diff --git a/svc/frontline/internal/errorpage/interface.go b/svc/frontline/internal/errorpage/interface.go new file mode 100644 index 0000000000..5c65161f49 --- /dev/null +++ b/svc/frontline/internal/errorpage/interface.go @@ -0,0 +1,27 @@ +package errorpage + +// Data contains all the fields available to the error page template. +type Data struct { + // StatusCode is the HTTP status code (e.g. 401, 502). + StatusCode int + + // Title is the human-readable status text (e.g. "Unauthorized"). + Title string + + // Message is a longer explanation shown to the user. + Message string + + // ErrorCode is the URN-style error code (e.g. "err:sentinel:unauthorized:invalid_key"). + ErrorCode string + + // DocsURL links to documentation for this error code. Empty if unavailable. + DocsURL string + + // RequestID is the frontline request ID for support reference. + RequestID string +} + +// Renderer renders an HTML error page from [Data]. +type Renderer interface { + Render(data Data) ([]byte, error) +} diff --git a/svc/frontline/middleware/BUILD.bazel b/svc/frontline/middleware/BUILD.bazel index c2158be8cf..ad2cafee4a 100644 --- a/svc/frontline/middleware/BUILD.bazel +++ b/svc/frontline/middleware/BUILD.bazel @@ -11,6 +11,7 @@ go_library( "//pkg/logger", "//pkg/otel/tracing", "//pkg/zen", + "//svc/frontline/internal/errorpage", "@com_github_prometheus_client_golang//prometheus", "@com_github_prometheus_client_golang//prometheus/promauto", "@io_opentelemetry_go_otel//attribute", diff --git a/svc/frontline/middleware/observability.go b/svc/frontline/middleware/observability.go index 9c8ac1685c..29aa743cb4 100644 --- a/svc/frontline/middleware/observability.go +++ b/svc/frontline/middleware/observability.go @@ -2,8 +2,6 @@ package middleware import ( "context" - "fmt" - "html" "net/http" "strconv" "strings" @@ -16,6 +14,7 @@ import ( "github.com/unkeyed/unkey/pkg/logger" "github.com/unkeyed/unkey/pkg/otel/tracing" "github.com/unkeyed/unkey/pkg/zen" + "github.com/unkeyed/unkey/svc/frontline/internal/errorpage" "go.opentelemetry.io/otel/attribute" ) @@ -102,7 +101,7 @@ func categorizeErrorTypeFrontline(urn codes.URN, statusCode int, hasError bool) return "unknown" } -func WithObservability(region string) zen.Middleware { +func WithObservability(region string, renderer errorpage.Renderer) zen.Middleware { return func(next zen.HandleFunc) zen.HandleFunc { return func(ctx context.Context, s *zen.Session) error { startTime := time.Now() @@ -180,7 +179,25 @@ func WithObservability(region string) zen.Middleware { }, }) } else { - writeErr = s.HTML(pageInfo.Status, renderErrorHTMLFrontline(title, userMessage, string(code.URN()))) + htmlBody, renderErr := renderer.Render(errorpage.Data{ + StatusCode: pageInfo.Status, + Title: title, + Message: userMessage, + ErrorCode: string(code.URN()), + DocsURL: code.DocsURL(), + RequestID: s.RequestID(), + }) + if renderErr != nil { + logger.Error("failed to render error page", "error", renderErr.Error()) + writeErr = s.JSON(pageInfo.Status, ErrorResponse{ + Error: ErrorDetail{ + Code: string(code.URN()), + Message: userMessage, + }, + }) + } else { + writeErr = s.HTML(pageInfo.Status, htmlBody) + } } if writeErr != nil { @@ -256,29 +273,3 @@ func getErrorPageInfoFrontline(urn codes.URN) errorPageInfo { } } } - -func renderErrorHTMLFrontline(title, message, errorCode string) []byte { - escapedTitle := html.EscapeString(title) - escapedMessage := html.EscapeString(message) - escapedErrorCode := html.EscapeString(errorCode) - - return fmt.Appendf(nil, ` - - - - - %s - - - -

%s

-

%s

-

Error: %s

- -`, escapedTitle, escapedTitle, escapedMessage, escapedErrorCode) -} diff --git a/svc/frontline/routes/BUILD.bazel b/svc/frontline/routes/BUILD.bazel index 94c834c7d6..6014aabb9d 100644 --- a/svc/frontline/routes/BUILD.bazel +++ b/svc/frontline/routes/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "//gen/rpc/ctrl", "//pkg/clock", "//pkg/zen", + "//svc/frontline/internal/errorpage", "//svc/frontline/middleware", "//svc/frontline/routes/acme", "//svc/frontline/routes/internal_health", diff --git a/svc/frontline/routes/register.go b/svc/frontline/routes/register.go index 360aa0faf3..f686df3405 100644 --- a/svc/frontline/routes/register.go +++ b/svc/frontline/routes/register.go @@ -14,7 +14,7 @@ import ( func Register(srv *zen.Server, svc *Services) { withLogging := zen.WithLogging(zen.SkipPaths("/_unkey/internal/", "/health/")) withPanicRecovery := zen.WithPanicRecovery() - withObservability := middleware.WithObservability(svc.Region) + withObservability := middleware.WithObservability(svc.Region, svc.ErrorPageRenderer) withTimeout := zen.WithTimeout(5 * time.Minute) defaultMiddlewares := []zen.Middleware{ @@ -56,7 +56,7 @@ func RegisterChallengeServer(srv *zen.Server, svc *Services) { []zen.Middleware{ zen.WithPanicRecovery(), withLogging, - middleware.WithObservability(svc.Region), + middleware.WithObservability(svc.Region, svc.ErrorPageRenderer), }, &acme.Handler{ RouterService: svc.RouterService, diff --git a/svc/frontline/routes/services.go b/svc/frontline/routes/services.go index fe9d36b812..a43ef7b7cb 100644 --- a/svc/frontline/routes/services.go +++ b/svc/frontline/routes/services.go @@ -3,14 +3,16 @@ package routes import ( "github.com/unkeyed/unkey/gen/rpc/ctrl" "github.com/unkeyed/unkey/pkg/clock" + "github.com/unkeyed/unkey/svc/frontline/internal/errorpage" "github.com/unkeyed/unkey/svc/frontline/services/proxy" "github.com/unkeyed/unkey/svc/frontline/services/router" ) type Services struct { - Region string - RouterService router.Service - ProxyService proxy.Service - Clock clock.Clock - AcmeClient ctrl.AcmeServiceClient + Region string + RouterService router.Service + ProxyService proxy.Service + Clock clock.Clock + AcmeClient ctrl.AcmeServiceClient + ErrorPageRenderer errorpage.Renderer } diff --git a/svc/frontline/run.go b/svc/frontline/run.go index e2ed34ccb5..8c93e1e4ec 100644 --- a/svc/frontline/run.go +++ b/svc/frontline/run.go @@ -28,6 +28,7 @@ import ( pkgtls "github.com/unkeyed/unkey/pkg/tls" "github.com/unkeyed/unkey/pkg/version" "github.com/unkeyed/unkey/pkg/zen" + "github.com/unkeyed/unkey/svc/frontline/internal/errorpage" "github.com/unkeyed/unkey/svc/frontline/routes" "github.com/unkeyed/unkey/svc/frontline/services/caches" "github.com/unkeyed/unkey/svc/frontline/services/certmanager" @@ -259,11 +260,12 @@ func Run(ctx context.Context, cfg Config) error { acmeClient := ctrl.NewConnectAcmeServiceClient(ctrlv1connect.NewAcmeServiceClient(ptr.P(http.Client{}), cfg.CtrlAddr)) svcs := &routes.Services{ - Region: cfg.Region, - RouterService: routerSvc, - ProxyService: proxySvc, - Clock: clk, - AcmeClient: acmeClient, + Region: cfg.Region, + RouterService: routerSvc, + ProxyService: proxySvc, + Clock: clk, + AcmeClient: acmeClient, + ErrorPageRenderer: errorpage.NewRenderer(), } // Start HTTPS frontline server (main proxy server) diff --git a/svc/frontline/services/proxy/BUILD.bazel b/svc/frontline/services/proxy/BUILD.bazel index 11a8451438..597312290b 100644 --- a/svc/frontline/services/proxy/BUILD.bazel +++ b/svc/frontline/services/proxy/BUILD.bazel @@ -23,6 +23,7 @@ go_library( "//pkg/logger", "//pkg/timing", "//pkg/zen", + "//svc/frontline/internal/errorpage", "@org_golang_x_net//http2", ], ) diff --git a/svc/frontline/services/proxy/forward.go b/svc/frontline/services/proxy/forward.go index 2f7752311e..6bb0cf8b30 100644 --- a/svc/frontline/services/proxy/forward.go +++ b/svc/frontline/services/proxy/forward.go @@ -1,10 +1,14 @@ package proxy import ( + "bytes" + "encoding/json" "fmt" + "io" "net/http" "net/http/httputil" "net/url" + "strings" "time" "github.com/unkeyed/unkey/pkg/codes" @@ -12,6 +16,7 @@ import ( "github.com/unkeyed/unkey/pkg/logger" "github.com/unkeyed/unkey/pkg/timing" "github.com/unkeyed/unkey/pkg/zen" + "github.com/unkeyed/unkey/svc/frontline/internal/errorpage" ) type forwardConfig struct { @@ -81,11 +86,16 @@ func (s *service) forward(sess *zen.Session, cfg forwardConfig) error { }, }) - if resp.StatusCode >= 500 && resp.Header.Get("X-Unkey-Error-Source") == "sentinel" { - if sentinelTime := resp.Header.Get(timing.HeaderName); sentinelTime != "" { - sess.ResponseWriter().Header().Add(timing.HeaderName, sentinelTime) - } + if resp.Header.Get("X-Unkey-Error-Source") != "sentinel" { + return nil + } + + if sentinelTime := resp.Header.Get(timing.HeaderName); sentinelTime != "" { + sess.ResponseWriter().Header().Add(timing.HeaderName, sentinelTime) + } + // 5xx from sentinel → fault error → frontline observability handles content negotiation + if resp.StatusCode >= 500 { urn := codes.Frontline.Proxy.BadGateway.URN() switch resp.StatusCode { case http.StatusServiceUnavailable: @@ -103,6 +113,12 @@ func (s *service) forward(sess *zen.Session, cfg forwardConfig) error { ) } + // 4xx from sentinel (auth errors, rate limits) → rewrite to HTML if client prefers it, + // otherwise pass the JSON through untouched. + if resp.StatusCode >= 400 && wantsHTML(sess.Request()) { + return rewriteSentinelErrorAsHTML(resp, sess.RequestID(), s.errorPageRenderer) + } + return nil }, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { @@ -134,3 +150,86 @@ func (s *service) forward(sess *zen.Session, cfg forwardConfig) error { return nil } + +// wantsHTML returns true if the client prefers HTML over JSON based on the Accept header. +func wantsHTML(r *http.Request) bool { + accept := r.Header.Get("Accept") + if accept == "" { + return false + } + + for _, part := range strings.Split(accept, ",") { + mediaType := strings.TrimSpace(strings.SplitN(part, ";", 2)[0]) + switch mediaType { + case "text/html": + return true + case "application/json", "application/*", "*/*": + return false + } + } + + return false +} + +// sentinelError matches the JSON error structure returned by sentinel. +type sentinelError struct { + Error struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error"` +} + +// rewriteSentinelErrorAsHTML reads the sentinel JSON error response and replaces +// the body with a styled HTML error page. The original status code is preserved. +func rewriteSentinelErrorAsHTML(resp *http.Response, requestID string, renderer errorpage.Renderer) error { + body, err := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + return nil // can't read body, let it pass through + } + + var parsed sentinelError + if err := json.Unmarshal(body, &parsed); err != nil { + // Not valid JSON, put the body back unchanged + resp.Body = io.NopCloser(bytes.NewReader(body)) + return nil + } + + message := parsed.Error.Message + if message == "" { + message = http.StatusText(resp.StatusCode) + } + + title := http.StatusText(resp.StatusCode) + if title == "" { + title = "Error" + } + + var docsURL string + if parsed.Error.Code != "" { + if code, parseErr := codes.ParseCode(parsed.Error.Code); parseErr == nil { + docsURL = code.DocsURL() + } + } + + htmlBody, renderErr := renderer.Render(errorpage.Data{ + StatusCode: resp.StatusCode, + Title: title, + Message: message, + ErrorCode: parsed.Error.Code, + DocsURL: docsURL, + RequestID: requestID, + }) + if renderErr != nil { + // Template render failed, put original body back + resp.Body = io.NopCloser(bytes.NewReader(body)) + return nil + } + + resp.Body = io.NopCloser(bytes.NewReader(htmlBody)) + resp.ContentLength = int64(len(htmlBody)) + resp.Header.Set("Content-Type", "text/html; charset=utf-8") + resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(htmlBody))) + + return nil +} diff --git a/svc/frontline/services/proxy/interface.go b/svc/frontline/services/proxy/interface.go index ec696cf8fd..a2b32527e8 100644 --- a/svc/frontline/services/proxy/interface.go +++ b/svc/frontline/services/proxy/interface.go @@ -8,6 +8,7 @@ import ( "github.com/unkeyed/unkey/pkg/clock" "github.com/unkeyed/unkey/pkg/db" "github.com/unkeyed/unkey/pkg/zen" + "github.com/unkeyed/unkey/svc/frontline/internal/errorpage" ) // Service defines the interface for proxying requests to sentinels or remote NLBs. @@ -54,4 +55,7 @@ type Config struct { // Transport allows passing a shared HTTP transport for connection pooling // If nil, a new transport will be created with the other config values Transport *http.Transport + + // ErrorPageRenderer renders HTML error pages for sentinel errors. + ErrorPageRenderer errorpage.Renderer } diff --git a/svc/frontline/services/proxy/service.go b/svc/frontline/services/proxy/service.go index bf4e85090f..b829d54174 100644 --- a/svc/frontline/services/proxy/service.go +++ b/svc/frontline/services/proxy/service.go @@ -16,17 +16,19 @@ import ( "github.com/unkeyed/unkey/pkg/fault" "github.com/unkeyed/unkey/pkg/logger" "github.com/unkeyed/unkey/pkg/zen" + "github.com/unkeyed/unkey/svc/frontline/internal/errorpage" "golang.org/x/net/http2" ) type service struct { - instanceID string - region string - apexDomain string - clock clock.Clock - transport *http.Transport - h2cTransport *http2.Transport - maxHops int + instanceID string + region string + apexDomain string + clock clock.Clock + transport *http.Transport + h2cTransport *http2.Transport + maxHops int + errorPageRenderer errorpage.Renderer } var _ Service = (*service)(nil) @@ -90,14 +92,20 @@ func New(cfg Config) (*service, error) { }, } + renderer := cfg.ErrorPageRenderer + if renderer == nil { + renderer = errorpage.NewRenderer() + } + return &service{ - instanceID: cfg.InstanceID, - region: cfg.Region, - apexDomain: cfg.ApexDomain, - clock: cfg.Clock, - transport: transport, - h2cTransport: h2cTransport, - maxHops: maxHops, + instanceID: cfg.InstanceID, + region: cfg.Region, + apexDomain: cfg.ApexDomain, + clock: cfg.Clock, + transport: transport, + h2cTransport: h2cTransport, + maxHops: maxHops, + errorPageRenderer: renderer, }, nil } diff --git a/svc/sentinel/middleware/observability.go b/svc/sentinel/middleware/observability.go index 3f8d192bdb..a846d48bb9 100644 --- a/svc/sentinel/middleware/observability.go +++ b/svc/sentinel/middleware/observability.go @@ -13,6 +13,7 @@ import ( "github.com/unkeyed/unkey/pkg/logger" "github.com/unkeyed/unkey/pkg/otel/tracing" "github.com/unkeyed/unkey/pkg/zen" + handler "github.com/unkeyed/unkey/svc/sentinel/routes/proxy" "go.opentelemetry.io/otel/attribute" ) @@ -245,6 +246,12 @@ func WithObservability(environmentID, region string) zen.Middleware { pageInfo := getErrorPageInfo(urn) statusCode = pageInfo.Status + // Ensure tracking has the resolved status for CH logging, + // in case WithProxyErrorHandling didn't set it (e.g. auth errors). + if tracking, ok := handler.SentinelTrackingFromContext(ctx); ok && tracking.ResponseStatus == 0 { + tracking.ResponseStatus = int32(statusCode) + } + errorType = categorizeErrorType(urn, statusCode, hasError) userMessage := pageInfo.Message From b7fb1e3953124772fb8f95c376a7e76528d2af17 Mon Sep 17 00:00:00 2001 From: Flo Date: Thu, 19 Feb 2026 12:10:10 +0100 Subject: [PATCH 3/8] add rl headers. --- svc/sentinel/engine/keyauth.go | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/svc/sentinel/engine/keyauth.go b/svc/sentinel/engine/keyauth.go index cdcce673f8..f6caf74720 100644 --- a/svc/sentinel/engine/keyauth.go +++ b/svc/sentinel/engine/keyauth.go @@ -2,7 +2,9 @@ package engine import ( "context" + "math" "net/http" + "strconv" "time" sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" @@ -99,6 +101,10 @@ func (e *KeyAuthExecutor) Execute( ) } + // Write rate limit headers before checking status so they're present + // on both success (2xx) and rate-limited (429) responses. + writeRateLimitHeaders(sess.ResponseWriter(), verifier.RatelimitResults, e.clock) + // Check post-verification status switch verifier.Status { case keys.StatusValid: @@ -167,3 +173,43 @@ func (e *KeyAuthExecutor) Execute( Claims: claims, }, nil } + +// writeRateLimitHeaders sets standard rate limit headers on the response. +// When multiple rate limits exist, it uses the most restrictive one (lowest remaining). +func writeRateLimitHeaders(w http.ResponseWriter, results map[string]keys.RatelimitConfigAndResult, clk clock.Clock) { + if len(results) == 0 { + return + } + + // Find the most restrictive rate limit (lowest remaining). + var mostRestrictive *keys.RatelimitConfigAndResult + for _, r := range results { + if r.Response == nil { + continue + } + + if mostRestrictive == nil || r.Response.Remaining < mostRestrictive.Response.Remaining { + rCopy := r + mostRestrictive = &rCopy + } + } + + if mostRestrictive == nil { + return + } + + resp := mostRestrictive.Response + h := w.Header() + h.Set("X-RateLimit-Limit", strconv.FormatInt(resp.Limit, 10)) + h.Set("X-RateLimit-Remaining", strconv.FormatInt(resp.Remaining, 10)) + h.Set("X-RateLimit-Reset", strconv.FormatInt(resp.Reset.Unix(), 10)) + + if !resp.Success { + retryAfter := math.Ceil(resp.Reset.Sub(clk.Now()).Seconds()) + if retryAfter < 1 { + retryAfter = 1 + } + + h.Set("Retry-After", strconv.FormatInt(int64(retryAfter), 10)) + } +} From 0a6acc078fd3e945739d6ab6fa2f9eab5f49bdab Mon Sep 17 00:00:00 2001 From: chronark Date: Thu, 19 Feb 2026 16:26:39 +0100 Subject: [PATCH 4/8] feat: new ui and fixed a bunch of stuff --- gen/proto/sentinel/v1/BUILD.bazel | 3 +- gen/proto/sentinel/v1/basicauth.pb.go | 58 +-- gen/proto/sentinel/v1/config.pb.go | 134 ++++++ gen/proto/sentinel/v1/iprules.pb.go | 52 +-- gen/proto/sentinel/v1/jwtauth.pb.go | 54 +-- gen/proto/sentinel/v1/keyauth.pb.go | 118 +++-- gen/proto/sentinel/v1/match.pb.go | 90 ++-- gen/proto/sentinel/v1/middleware.pb.go | 411 ------------------ gen/proto/sentinel/v1/openapi.pb.go | 52 +-- gen/proto/sentinel/v1/policy.pb.go | 326 ++++++++++++++ gen/proto/sentinel/v1/principal.pb.go | 62 +-- gen/proto/sentinel/v1/ratelimit.pb.go | 90 ++-- ...onment_find_with_settings.sql_generated.go | 74 ++-- ...gs_find_by_environment_id.sql_generated.go | 5 +- pkg/db/models_generated.go | 1 + pkg/db/querier_generated.go | 17 +- .../environment_find_with_settings.sql | 15 +- pkg/db/schema.sql | 1 + svc/ctrl/api/github_webhook.go | 18 +- .../services/deployment/create_deployment.go | 14 +- svc/krane/internal/sentinel/apply.go | 8 + svc/sentinel/engine/BUILD.bazel | 1 - svc/sentinel/engine/engine.go | 31 +- svc/sentinel/engine/engine_test.go | 46 +- svc/sentinel/engine/integration_test.go | 267 ++++++------ svc/sentinel/engine/keyauth.go | 17 +- svc/sentinel/engine/match.go | 2 +- svc/sentinel/engine/match_test.go | 35 +- svc/sentinel/proto/config/v1/config.proto | 19 + svc/sentinel/proto/generate.go | 6 +- .../v1/basicauth.proto | 0 .../{middleware => policies}/v1/iprules.proto | 0 .../{middleware => policies}/v1/jwtauth.proto | 0 .../{middleware => policies}/v1/keyauth.proto | 14 +- .../{middleware => policies}/v1/match.proto | 0 .../{middleware => policies}/v1/openapi.proto | 0 .../v1/policy.proto} | 47 +- .../v1/principal.proto | 0 .../v1/ratelimit.proto | 0 svc/sentinel/routes/proxy/handler.go | 5 +- .../sentinel-settings/keyspaces.tsx | 223 ++++++++++ .../[projectId]/(overview)/settings/page.tsx | 11 +- .../gen/proto/config/v1/config_pb.ts | 43 ++ .../gen/proto/middleware/v1/middleware_pb.ts | 187 -------- .../v1/basicauth_pb.ts | 12 +- .../{middleware => policies}/v1/iprules_pb.ts | 10 +- .../{middleware => policies}/v1/jwtauth_pb.ts | 10 +- .../{middleware => policies}/v1/keyauth_pb.ts | 38 +- .../{middleware => policies}/v1/match_pb.ts | 20 +- .../{middleware => policies}/v1/openapi_pb.ts | 10 +- .../gen/proto/policies/v1/policy_pb.ts | 135 ++++++ .../v1/principal_pb.ts | 12 +- .../v1/ratelimit_pb.ts | 22 +- .../get-available-keyspaces.ts | 26 ++ .../deploy/environment-settings/get.ts | 13 +- .../sentinel/update-middleware.ts | 66 +++ web/apps/dashboard/lib/trpc/routers/index.ts | 6 + .../schema/environment_runtime_settings.ts | 3 + web/internal/db/src/schema/environments.ts | 2 + 59 files changed, 1652 insertions(+), 1290 deletions(-) create mode 100644 gen/proto/sentinel/v1/config.pb.go delete mode 100644 gen/proto/sentinel/v1/middleware.pb.go create mode 100644 gen/proto/sentinel/v1/policy.pb.go create mode 100644 svc/sentinel/proto/config/v1/config.proto rename svc/sentinel/proto/{middleware => policies}/v1/basicauth.proto (100%) rename svc/sentinel/proto/{middleware => policies}/v1/iprules.proto (100%) rename svc/sentinel/proto/{middleware => policies}/v1/jwtauth.proto (100%) rename svc/sentinel/proto/{middleware => policies}/v1/keyauth.proto (90%) rename svc/sentinel/proto/{middleware => policies}/v1/match.proto (100%) rename svc/sentinel/proto/{middleware => policies}/v1/openapi.proto (100%) rename svc/sentinel/proto/{middleware/v1/middleware.proto => policies/v1/policy.proto} (52%) rename svc/sentinel/proto/{middleware => policies}/v1/principal.proto (100%) rename svc/sentinel/proto/{middleware => policies}/v1/ratelimit.proto (100%) create mode 100644 web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/sentinel-settings/keyspaces.tsx create mode 100644 web/apps/dashboard/gen/proto/config/v1/config_pb.ts delete mode 100644 web/apps/dashboard/gen/proto/middleware/v1/middleware_pb.ts rename web/apps/dashboard/gen/proto/{middleware => policies}/v1/basicauth_pb.ts (81%) rename web/apps/dashboard/gen/proto/{middleware => policies}/v1/iprules_pb.ts (79%) rename web/apps/dashboard/gen/proto/{middleware => policies}/v1/jwtauth_pb.ts (87%) rename web/apps/dashboard/gen/proto/{middleware => policies}/v1/keyauth_pb.ts (79%) rename web/apps/dashboard/gen/proto/{middleware => policies}/v1/match_pb.ts (84%) rename web/apps/dashboard/gen/proto/{middleware => policies}/v1/openapi_pb.ts (78%) create mode 100644 web/apps/dashboard/gen/proto/policies/v1/policy_pb.ts rename web/apps/dashboard/gen/proto/{middleware => policies}/v1/principal_pb.ts (80%) rename web/apps/dashboard/gen/proto/{middleware => policies}/v1/ratelimit_pb.ts (84%) create mode 100644 web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/get-available-keyspaces.ts create mode 100644 web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/sentinel/update-middleware.ts diff --git a/gen/proto/sentinel/v1/BUILD.bazel b/gen/proto/sentinel/v1/BUILD.bazel index 0622bf4065..f34c748585 100644 --- a/gen/proto/sentinel/v1/BUILD.bazel +++ b/gen/proto/sentinel/v1/BUILD.bazel @@ -4,13 +4,14 @@ go_library( name = "sentinel", srcs = [ "basicauth.pb.go", + "config.pb.go", "iprules.pb.go", "jwtauth.pb.go", "keyauth.pb.go", "match.pb.go", - "middleware.pb.go", "oneof_interfaces.go", "openapi.pb.go", + "policy.pb.go", "principal.pb.go", "ratelimit.pb.go", ], diff --git a/gen/proto/sentinel/v1/basicauth.pb.go b/gen/proto/sentinel/v1/basicauth.pb.go index e6d72ce8d2..847b2141c3 100644 --- a/gen/proto/sentinel/v1/basicauth.pb.go +++ b/gen/proto/sentinel/v1/basicauth.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.36.8 // protoc (unknown) -// source: middleware/v1/basicauth.proto +// source: policies/v1/basicauth.proto package sentinelv1 @@ -55,7 +55,7 @@ type BasicAuth struct { func (x *BasicAuth) Reset() { *x = BasicAuth{} - mi := &file_middleware_v1_basicauth_proto_msgTypes[0] + mi := &file_policies_v1_basicauth_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -67,7 +67,7 @@ func (x *BasicAuth) String() string { func (*BasicAuth) ProtoMessage() {} func (x *BasicAuth) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_basicauth_proto_msgTypes[0] + mi := &file_policies_v1_basicauth_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -80,7 +80,7 @@ func (x *BasicAuth) ProtoReflect() protoreflect.Message { // Deprecated: Use BasicAuth.ProtoReflect.Descriptor instead. func (*BasicAuth) Descriptor() ([]byte, []int) { - return file_middleware_v1_basicauth_proto_rawDescGZIP(), []int{0} + return file_policies_v1_basicauth_proto_rawDescGZIP(), []int{0} } func (x *BasicAuth) GetCredentials() []*BasicAuthCredential { @@ -110,7 +110,7 @@ type BasicAuthCredential struct { func (x *BasicAuthCredential) Reset() { *x = BasicAuthCredential{} - mi := &file_middleware_v1_basicauth_proto_msgTypes[1] + mi := &file_policies_v1_basicauth_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -122,7 +122,7 @@ func (x *BasicAuthCredential) String() string { func (*BasicAuthCredential) ProtoMessage() {} func (x *BasicAuthCredential) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_basicauth_proto_msgTypes[1] + mi := &file_policies_v1_basicauth_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -135,7 +135,7 @@ func (x *BasicAuthCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use BasicAuthCredential.ProtoReflect.Descriptor instead. func (*BasicAuthCredential) Descriptor() ([]byte, []int) { - return file_middleware_v1_basicauth_proto_rawDescGZIP(), []int{1} + return file_policies_v1_basicauth_proto_rawDescGZIP(), []int{1} } func (x *BasicAuthCredential) GetUsername() string { @@ -152,11 +152,11 @@ func (x *BasicAuthCredential) GetPasswordHash() string { return "" } -var File_middleware_v1_basicauth_proto protoreflect.FileDescriptor +var File_policies_v1_basicauth_proto protoreflect.FileDescriptor -const file_middleware_v1_basicauth_proto_rawDesc = "" + +const file_policies_v1_basicauth_proto_rawDesc = "" + "\n" + - "\x1dmiddleware/v1/basicauth.proto\x12\vsentinel.v1\"O\n" + + "\x1bpolicies/v1/basicauth.proto\x12\vsentinel.v1\"O\n" + "\tBasicAuth\x12B\n" + "\vcredentials\x18\x01 \x03(\v2 .sentinel.v1.BasicAuthCredentialR\vcredentials\"V\n" + "\x13BasicAuthCredential\x12\x1a\n" + @@ -165,23 +165,23 @@ const file_middleware_v1_basicauth_proto_rawDesc = "" + "\x0fcom.sentinel.v1B\x0eBasicauthProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" var ( - file_middleware_v1_basicauth_proto_rawDescOnce sync.Once - file_middleware_v1_basicauth_proto_rawDescData []byte + file_policies_v1_basicauth_proto_rawDescOnce sync.Once + file_policies_v1_basicauth_proto_rawDescData []byte ) -func file_middleware_v1_basicauth_proto_rawDescGZIP() []byte { - file_middleware_v1_basicauth_proto_rawDescOnce.Do(func() { - file_middleware_v1_basicauth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_middleware_v1_basicauth_proto_rawDesc), len(file_middleware_v1_basicauth_proto_rawDesc))) +func file_policies_v1_basicauth_proto_rawDescGZIP() []byte { + file_policies_v1_basicauth_proto_rawDescOnce.Do(func() { + file_policies_v1_basicauth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_policies_v1_basicauth_proto_rawDesc), len(file_policies_v1_basicauth_proto_rawDesc))) }) - return file_middleware_v1_basicauth_proto_rawDescData + return file_policies_v1_basicauth_proto_rawDescData } -var file_middleware_v1_basicauth_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_middleware_v1_basicauth_proto_goTypes = []any{ +var file_policies_v1_basicauth_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_policies_v1_basicauth_proto_goTypes = []any{ (*BasicAuth)(nil), // 0: sentinel.v1.BasicAuth (*BasicAuthCredential)(nil), // 1: sentinel.v1.BasicAuthCredential } -var file_middleware_v1_basicauth_proto_depIdxs = []int32{ +var file_policies_v1_basicauth_proto_depIdxs = []int32{ 1, // 0: sentinel.v1.BasicAuth.credentials:type_name -> sentinel.v1.BasicAuthCredential 1, // [1:1] is the sub-list for method output_type 1, // [1:1] is the sub-list for method input_type @@ -190,26 +190,26 @@ var file_middleware_v1_basicauth_proto_depIdxs = []int32{ 0, // [0:1] is the sub-list for field type_name } -func init() { file_middleware_v1_basicauth_proto_init() } -func file_middleware_v1_basicauth_proto_init() { - if File_middleware_v1_basicauth_proto != nil { +func init() { file_policies_v1_basicauth_proto_init() } +func file_policies_v1_basicauth_proto_init() { + if File_policies_v1_basicauth_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_middleware_v1_basicauth_proto_rawDesc), len(file_middleware_v1_basicauth_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_policies_v1_basicauth_proto_rawDesc), len(file_policies_v1_basicauth_proto_rawDesc)), NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_middleware_v1_basicauth_proto_goTypes, - DependencyIndexes: file_middleware_v1_basicauth_proto_depIdxs, - MessageInfos: file_middleware_v1_basicauth_proto_msgTypes, + GoTypes: file_policies_v1_basicauth_proto_goTypes, + DependencyIndexes: file_policies_v1_basicauth_proto_depIdxs, + MessageInfos: file_policies_v1_basicauth_proto_msgTypes, }.Build() - File_middleware_v1_basicauth_proto = out.File - file_middleware_v1_basicauth_proto_goTypes = nil - file_middleware_v1_basicauth_proto_depIdxs = nil + File_policies_v1_basicauth_proto = out.File + file_policies_v1_basicauth_proto_goTypes = nil + file_policies_v1_basicauth_proto_depIdxs = nil } diff --git a/gen/proto/sentinel/v1/config.pb.go b/gen/proto/sentinel/v1/config.pb.go new file mode 100644 index 0000000000..c3c067b2c8 --- /dev/null +++ b/gen/proto/sentinel/v1/config.pb.go @@ -0,0 +1,134 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc (unknown) +// source: config/v1/config.proto + +package sentinelv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Config defines the middleware pipeline for a sentinel deployment. Each +// policy in the list is evaluated in order, forming a chain of request +// processing stages like authentication, rate limiting, and request validation. +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Policies are the middleware layers to apply to incoming requests, in + // evaluation order. Each [Policy] combines a match expression (which + // requests it applies to) with a configuration (what it does). Policies + // are evaluated sequentially; if any policy rejects the request, the + // chain short-circuits and returns an error to the client. + Policies []*Policy `protobuf:"bytes,1,rep,name=policies,proto3" json:"policies,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_config_v1_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_config_v1_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_config_v1_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetPolicies() []*Policy { + if x != nil { + return x.Policies + } + return nil +} + +var File_config_v1_config_proto protoreflect.FileDescriptor + +const file_config_v1_config_proto_rawDesc = "" + + "\n" + + "\x16config/v1/config.proto\x12\vsentinel.v1\x1a\x18policies/v1/policy.proto\"9\n" + + "\x06Config\x12/\n" + + "\bpolicies\x18\x01 \x03(\v2\x13.sentinel.v1.PolicyR\bpoliciesB\xa6\x01\n" + + "\x0fcom.sentinel.v1B\vConfigProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" + +var ( + file_config_v1_config_proto_rawDescOnce sync.Once + file_config_v1_config_proto_rawDescData []byte +) + +func file_config_v1_config_proto_rawDescGZIP() []byte { + file_config_v1_config_proto_rawDescOnce.Do(func() { + file_config_v1_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_config_v1_config_proto_rawDesc), len(file_config_v1_config_proto_rawDesc))) + }) + return file_config_v1_config_proto_rawDescData +} + +var file_config_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_config_v1_config_proto_goTypes = []any{ + (*Config)(nil), // 0: sentinel.v1.Config + (*Policy)(nil), // 1: sentinel.v1.Policy +} +var file_config_v1_config_proto_depIdxs = []int32{ + 1, // 0: sentinel.v1.Config.policies:type_name -> sentinel.v1.Policy + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_config_v1_config_proto_init() } +func file_config_v1_config_proto_init() { + if File_config_v1_config_proto != nil { + return + } + file_policies_v1_policy_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_config_v1_config_proto_rawDesc), len(file_config_v1_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_config_v1_config_proto_goTypes, + DependencyIndexes: file_config_v1_config_proto_depIdxs, + MessageInfos: file_config_v1_config_proto_msgTypes, + }.Build() + File_config_v1_config_proto = out.File + file_config_v1_config_proto_goTypes = nil + file_config_v1_config_proto_depIdxs = nil +} diff --git a/gen/proto/sentinel/v1/iprules.pb.go b/gen/proto/sentinel/v1/iprules.pb.go index a9d44699ff..a0f996dfe4 100644 --- a/gen/proto/sentinel/v1/iprules.pb.go +++ b/gen/proto/sentinel/v1/iprules.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.36.8 // protoc (unknown) -// source: middleware/v1/iprules.proto +// source: policies/v1/iprules.proto package sentinelv1 @@ -60,7 +60,7 @@ type IPRules struct { func (x *IPRules) Reset() { *x = IPRules{} - mi := &file_middleware_v1_iprules_proto_msgTypes[0] + mi := &file_policies_v1_iprules_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -72,7 +72,7 @@ func (x *IPRules) String() string { func (*IPRules) ProtoMessage() {} func (x *IPRules) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_iprules_proto_msgTypes[0] + mi := &file_policies_v1_iprules_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -85,7 +85,7 @@ func (x *IPRules) ProtoReflect() protoreflect.Message { // Deprecated: Use IPRules.ProtoReflect.Descriptor instead. func (*IPRules) Descriptor() ([]byte, []int) { - return file_middleware_v1_iprules_proto_rawDescGZIP(), []int{0} + return file_policies_v1_iprules_proto_rawDescGZIP(), []int{0} } func (x *IPRules) GetAllow() []string { @@ -102,33 +102,33 @@ func (x *IPRules) GetDeny() []string { return nil } -var File_middleware_v1_iprules_proto protoreflect.FileDescriptor +var File_policies_v1_iprules_proto protoreflect.FileDescriptor -const file_middleware_v1_iprules_proto_rawDesc = "" + +const file_policies_v1_iprules_proto_rawDesc = "" + "\n" + - "\x1bmiddleware/v1/iprules.proto\x12\vsentinel.v1\"3\n" + + "\x19policies/v1/iprules.proto\x12\vsentinel.v1\"3\n" + "\aIPRules\x12\x14\n" + "\x05allow\x18\x01 \x03(\tR\x05allow\x12\x12\n" + "\x04deny\x18\x02 \x03(\tR\x04denyB\xa7\x01\n" + "\x0fcom.sentinel.v1B\fIprulesProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" var ( - file_middleware_v1_iprules_proto_rawDescOnce sync.Once - file_middleware_v1_iprules_proto_rawDescData []byte + file_policies_v1_iprules_proto_rawDescOnce sync.Once + file_policies_v1_iprules_proto_rawDescData []byte ) -func file_middleware_v1_iprules_proto_rawDescGZIP() []byte { - file_middleware_v1_iprules_proto_rawDescOnce.Do(func() { - file_middleware_v1_iprules_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_middleware_v1_iprules_proto_rawDesc), len(file_middleware_v1_iprules_proto_rawDesc))) +func file_policies_v1_iprules_proto_rawDescGZIP() []byte { + file_policies_v1_iprules_proto_rawDescOnce.Do(func() { + file_policies_v1_iprules_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_policies_v1_iprules_proto_rawDesc), len(file_policies_v1_iprules_proto_rawDesc))) }) - return file_middleware_v1_iprules_proto_rawDescData + return file_policies_v1_iprules_proto_rawDescData } -var file_middleware_v1_iprules_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_middleware_v1_iprules_proto_goTypes = []any{ +var file_policies_v1_iprules_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_policies_v1_iprules_proto_goTypes = []any{ (*IPRules)(nil), // 0: sentinel.v1.IPRules } -var file_middleware_v1_iprules_proto_depIdxs = []int32{ +var file_policies_v1_iprules_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name @@ -136,26 +136,26 @@ var file_middleware_v1_iprules_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for field type_name } -func init() { file_middleware_v1_iprules_proto_init() } -func file_middleware_v1_iprules_proto_init() { - if File_middleware_v1_iprules_proto != nil { +func init() { file_policies_v1_iprules_proto_init() } +func file_policies_v1_iprules_proto_init() { + if File_policies_v1_iprules_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_middleware_v1_iprules_proto_rawDesc), len(file_middleware_v1_iprules_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_policies_v1_iprules_proto_rawDesc), len(file_policies_v1_iprules_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_middleware_v1_iprules_proto_goTypes, - DependencyIndexes: file_middleware_v1_iprules_proto_depIdxs, - MessageInfos: file_middleware_v1_iprules_proto_msgTypes, + GoTypes: file_policies_v1_iprules_proto_goTypes, + DependencyIndexes: file_policies_v1_iprules_proto_depIdxs, + MessageInfos: file_policies_v1_iprules_proto_msgTypes, }.Build() - File_middleware_v1_iprules_proto = out.File - file_middleware_v1_iprules_proto_goTypes = nil - file_middleware_v1_iprules_proto_depIdxs = nil + File_policies_v1_iprules_proto = out.File + file_policies_v1_iprules_proto_goTypes = nil + file_policies_v1_iprules_proto_depIdxs = nil } diff --git a/gen/proto/sentinel/v1/jwtauth.pb.go b/gen/proto/sentinel/v1/jwtauth.pb.go index b418cf6f0c..d5c3ca8092 100644 --- a/gen/proto/sentinel/v1/jwtauth.pb.go +++ b/gen/proto/sentinel/v1/jwtauth.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.36.8 // protoc (unknown) -// source: middleware/v1/jwtauth.proto +// source: policies/v1/jwtauth.proto package sentinelv1 @@ -101,7 +101,7 @@ type JWTAuth struct { func (x *JWTAuth) Reset() { *x = JWTAuth{} - mi := &file_middleware_v1_jwtauth_proto_msgTypes[0] + mi := &file_policies_v1_jwtauth_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -113,7 +113,7 @@ func (x *JWTAuth) String() string { func (*JWTAuth) ProtoMessage() {} func (x *JWTAuth) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_jwtauth_proto_msgTypes[0] + mi := &file_policies_v1_jwtauth_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -126,7 +126,7 @@ func (x *JWTAuth) ProtoReflect() protoreflect.Message { // Deprecated: Use JWTAuth.ProtoReflect.Descriptor instead. func (*JWTAuth) Descriptor() ([]byte, []int) { - return file_middleware_v1_jwtauth_proto_rawDescGZIP(), []int{0} + return file_policies_v1_jwtauth_proto_rawDescGZIP(), []int{0} } func (x *JWTAuth) GetJwksSource() isJWTAuth_JwksSource { @@ -257,11 +257,11 @@ func (*JWTAuth_OidcIssuer) isJWTAuth_JwksSource() {} func (*JWTAuth_PublicKeyPem) isJWTAuth_JwksSource() {} -var File_middleware_v1_jwtauth_proto protoreflect.FileDescriptor +var File_policies_v1_jwtauth_proto protoreflect.FileDescriptor -const file_middleware_v1_jwtauth_proto_rawDesc = "" + +const file_policies_v1_jwtauth_proto_rawDesc = "" + "\n" + - "\x1bmiddleware/v1/jwtauth.proto\x12\vsentinel.v1\"\x93\x03\n" + + "\x19policies/v1/jwtauth.proto\x12\vsentinel.v1\"\x93\x03\n" + "\aJWTAuth\x12\x1b\n" + "\bjwks_uri\x18\x01 \x01(\tH\x00R\ajwksUri\x12!\n" + "\voidc_issuer\x18\x02 \x01(\tH\x00R\n" + @@ -282,22 +282,22 @@ const file_middleware_v1_jwtauth_proto_rawDesc = "" + "\x0fcom.sentinel.v1B\fJwtauthProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" var ( - file_middleware_v1_jwtauth_proto_rawDescOnce sync.Once - file_middleware_v1_jwtauth_proto_rawDescData []byte + file_policies_v1_jwtauth_proto_rawDescOnce sync.Once + file_policies_v1_jwtauth_proto_rawDescData []byte ) -func file_middleware_v1_jwtauth_proto_rawDescGZIP() []byte { - file_middleware_v1_jwtauth_proto_rawDescOnce.Do(func() { - file_middleware_v1_jwtauth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_middleware_v1_jwtauth_proto_rawDesc), len(file_middleware_v1_jwtauth_proto_rawDesc))) +func file_policies_v1_jwtauth_proto_rawDescGZIP() []byte { + file_policies_v1_jwtauth_proto_rawDescOnce.Do(func() { + file_policies_v1_jwtauth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_policies_v1_jwtauth_proto_rawDesc), len(file_policies_v1_jwtauth_proto_rawDesc))) }) - return file_middleware_v1_jwtauth_proto_rawDescData + return file_policies_v1_jwtauth_proto_rawDescData } -var file_middleware_v1_jwtauth_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_middleware_v1_jwtauth_proto_goTypes = []any{ +var file_policies_v1_jwtauth_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_policies_v1_jwtauth_proto_goTypes = []any{ (*JWTAuth)(nil), // 0: sentinel.v1.JWTAuth } -var file_middleware_v1_jwtauth_proto_depIdxs = []int32{ +var file_policies_v1_jwtauth_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name @@ -305,12 +305,12 @@ var file_middleware_v1_jwtauth_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for field type_name } -func init() { file_middleware_v1_jwtauth_proto_init() } -func file_middleware_v1_jwtauth_proto_init() { - if File_middleware_v1_jwtauth_proto != nil { +func init() { file_policies_v1_jwtauth_proto_init() } +func file_policies_v1_jwtauth_proto_init() { + if File_policies_v1_jwtauth_proto != nil { return } - file_middleware_v1_jwtauth_proto_msgTypes[0].OneofWrappers = []any{ + file_policies_v1_jwtauth_proto_msgTypes[0].OneofWrappers = []any{ (*JWTAuth_JwksUri)(nil), (*JWTAuth_OidcIssuer)(nil), (*JWTAuth_PublicKeyPem)(nil), @@ -319,17 +319,17 @@ func file_middleware_v1_jwtauth_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_middleware_v1_jwtauth_proto_rawDesc), len(file_middleware_v1_jwtauth_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_policies_v1_jwtauth_proto_rawDesc), len(file_policies_v1_jwtauth_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_middleware_v1_jwtauth_proto_goTypes, - DependencyIndexes: file_middleware_v1_jwtauth_proto_depIdxs, - MessageInfos: file_middleware_v1_jwtauth_proto_msgTypes, + GoTypes: file_policies_v1_jwtauth_proto_goTypes, + DependencyIndexes: file_policies_v1_jwtauth_proto_depIdxs, + MessageInfos: file_policies_v1_jwtauth_proto_msgTypes, }.Build() - File_middleware_v1_jwtauth_proto = out.File - file_middleware_v1_jwtauth_proto_goTypes = nil - file_middleware_v1_jwtauth_proto_depIdxs = nil + File_policies_v1_jwtauth_proto = out.File + file_policies_v1_jwtauth_proto_goTypes = nil + file_policies_v1_jwtauth_proto_depIdxs = nil } diff --git a/gen/proto/sentinel/v1/keyauth.pb.go b/gen/proto/sentinel/v1/keyauth.pb.go index 3fa146347a..12a7b52500 100644 --- a/gen/proto/sentinel/v1/keyauth.pb.go +++ b/gen/proto/sentinel/v1/keyauth.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.36.8 // protoc (unknown) -// source: middleware/v1/keyauth.proto +// source: policies/v1/keyauth.proto package sentinelv1 @@ -44,7 +44,7 @@ type KeyAuth struct { // The Unkey key space (API) ID to authenticate against. Each key space // contains a set of API keys with shared configuration. This determines // which keys are valid for this policy. - KeySpaceId string `protobuf:"bytes,1,opt,name=key_space_id,json=keySpaceId,proto3" json:"key_space_id,omitempty"` + KeySpaceIds []string `protobuf:"bytes,1,rep,name=key_space_ids,json=keySpaceIds,proto3" json:"key_space_ids,omitempty"` // Ordered list of locations to extract the API key from. Sentinel tries // each location in order and uses the first one that yields a non-empty // value. This allows APIs to support multiple key delivery mechanisms @@ -54,13 +54,6 @@ type KeyAuth struct { // If empty, defaults to extracting from the Authorization header as a // Bearer token, which is the most common convention for API authentication. Locations []*KeyLocation `protobuf:"bytes,2,rep,name=locations,proto3" json:"locations,omitempty"` - // When true, requests that do not contain a key in any of the configured - // locations are allowed through without authentication. No [Principal] is - // produced for anonymous requests. This enables mixed-auth endpoints where - // unauthenticated users get a restricted view and authenticated users get - // full access — the application checks for the presence of identity headers - // to decide. - AllowAnonymous bool `protobuf:"varint,3,opt,name=allow_anonymous,json=allowAnonymous,proto3" json:"allow_anonymous,omitempty"` // Optional permission query evaluated against the key's permissions // returned by Unkey's verify API. Uses the same query language as // pkg/rbac.ParseQuery: AND and OR operators with parenthesized grouping, @@ -81,14 +74,14 @@ type KeyAuth struct { // required permissions. When empty, no permission check is performed. // // Limits: maximum 1000 characters, maximum 100 permission terms. - PermissionQuery string `protobuf:"bytes,5,opt,name=permission_query,json=permissionQuery,proto3" json:"permission_query,omitempty"` + PermissionQuery *string `protobuf:"bytes,5,opt,name=permission_query,json=permissionQuery,proto3,oneof" json:"permission_query,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *KeyAuth) Reset() { *x = KeyAuth{} - mi := &file_middleware_v1_keyauth_proto_msgTypes[0] + mi := &file_policies_v1_keyauth_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -100,7 +93,7 @@ func (x *KeyAuth) String() string { func (*KeyAuth) ProtoMessage() {} func (x *KeyAuth) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_keyauth_proto_msgTypes[0] + mi := &file_policies_v1_keyauth_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -113,14 +106,14 @@ func (x *KeyAuth) ProtoReflect() protoreflect.Message { // Deprecated: Use KeyAuth.ProtoReflect.Descriptor instead. func (*KeyAuth) Descriptor() ([]byte, []int) { - return file_middleware_v1_keyauth_proto_rawDescGZIP(), []int{0} + return file_policies_v1_keyauth_proto_rawDescGZIP(), []int{0} } -func (x *KeyAuth) GetKeySpaceId() string { +func (x *KeyAuth) GetKeySpaceIds() []string { if x != nil { - return x.KeySpaceId + return x.KeySpaceIds } - return "" + return nil } func (x *KeyAuth) GetLocations() []*KeyLocation { @@ -130,16 +123,9 @@ func (x *KeyAuth) GetLocations() []*KeyLocation { return nil } -func (x *KeyAuth) GetAllowAnonymous() bool { - if x != nil { - return x.AllowAnonymous - } - return false -} - func (x *KeyAuth) GetPermissionQuery() string { - if x != nil { - return x.PermissionQuery + if x != nil && x.PermissionQuery != nil { + return *x.PermissionQuery } return "" } @@ -162,7 +148,7 @@ type KeyLocation struct { func (x *KeyLocation) Reset() { *x = KeyLocation{} - mi := &file_middleware_v1_keyauth_proto_msgTypes[1] + mi := &file_policies_v1_keyauth_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -174,7 +160,7 @@ func (x *KeyLocation) String() string { func (*KeyLocation) ProtoMessage() {} func (x *KeyLocation) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_keyauth_proto_msgTypes[1] + mi := &file_policies_v1_keyauth_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -187,7 +173,7 @@ func (x *KeyLocation) ProtoReflect() protoreflect.Message { // Deprecated: Use KeyLocation.ProtoReflect.Descriptor instead. func (*KeyLocation) Descriptor() ([]byte, []int) { - return file_middleware_v1_keyauth_proto_rawDescGZIP(), []int{1} + return file_policies_v1_keyauth_proto_rawDescGZIP(), []int{1} } func (x *KeyLocation) GetLocation() isKeyLocation_Location { @@ -265,7 +251,7 @@ type BearerTokenLocation struct { func (x *BearerTokenLocation) Reset() { *x = BearerTokenLocation{} - mi := &file_middleware_v1_keyauth_proto_msgTypes[2] + mi := &file_policies_v1_keyauth_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -277,7 +263,7 @@ func (x *BearerTokenLocation) String() string { func (*BearerTokenLocation) ProtoMessage() {} func (x *BearerTokenLocation) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_keyauth_proto_msgTypes[2] + mi := &file_policies_v1_keyauth_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -290,7 +276,7 @@ func (x *BearerTokenLocation) ProtoReflect() protoreflect.Message { // Deprecated: Use BearerTokenLocation.ProtoReflect.Descriptor instead. func (*BearerTokenLocation) Descriptor() ([]byte, []int) { - return file_middleware_v1_keyauth_proto_rawDescGZIP(), []int{2} + return file_policies_v1_keyauth_proto_rawDescGZIP(), []int{2} } // HeaderKeyLocation extracts the API key from a named request header. This @@ -312,7 +298,7 @@ type HeaderKeyLocation struct { func (x *HeaderKeyLocation) Reset() { *x = HeaderKeyLocation{} - mi := &file_middleware_v1_keyauth_proto_msgTypes[3] + mi := &file_policies_v1_keyauth_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -324,7 +310,7 @@ func (x *HeaderKeyLocation) String() string { func (*HeaderKeyLocation) ProtoMessage() {} func (x *HeaderKeyLocation) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_keyauth_proto_msgTypes[3] + mi := &file_policies_v1_keyauth_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -337,7 +323,7 @@ func (x *HeaderKeyLocation) ProtoReflect() protoreflect.Message { // Deprecated: Use HeaderKeyLocation.ProtoReflect.Descriptor instead. func (*HeaderKeyLocation) Descriptor() ([]byte, []int) { - return file_middleware_v1_keyauth_proto_rawDescGZIP(), []int{3} + return file_policies_v1_keyauth_proto_rawDescGZIP(), []int{3} } func (x *HeaderKeyLocation) GetName() string { @@ -365,7 +351,7 @@ type QueryParamKeyLocation struct { func (x *QueryParamKeyLocation) Reset() { *x = QueryParamKeyLocation{} - mi := &file_middleware_v1_keyauth_proto_msgTypes[4] + mi := &file_policies_v1_keyauth_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -377,7 +363,7 @@ func (x *QueryParamKeyLocation) String() string { func (*QueryParamKeyLocation) ProtoMessage() {} func (x *QueryParamKeyLocation) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_keyauth_proto_msgTypes[4] + mi := &file_policies_v1_keyauth_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -390,7 +376,7 @@ func (x *QueryParamKeyLocation) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryParamKeyLocation.ProtoReflect.Descriptor instead. func (*QueryParamKeyLocation) Descriptor() ([]byte, []int) { - return file_middleware_v1_keyauth_proto_rawDescGZIP(), []int{4} + return file_policies_v1_keyauth_proto_rawDescGZIP(), []int{4} } func (x *QueryParamKeyLocation) GetName() string { @@ -400,17 +386,16 @@ func (x *QueryParamKeyLocation) GetName() string { return "" } -var File_middleware_v1_keyauth_proto protoreflect.FileDescriptor +var File_policies_v1_keyauth_proto protoreflect.FileDescriptor -const file_middleware_v1_keyauth_proto_rawDesc = "" + +const file_policies_v1_keyauth_proto_rawDesc = "" + "\n" + - "\x1bmiddleware/v1/keyauth.proto\x12\vsentinel.v1\"\xb7\x01\n" + - "\aKeyAuth\x12 \n" + - "\fkey_space_id\x18\x01 \x01(\tR\n" + - "keySpaceId\x126\n" + - "\tlocations\x18\x02 \x03(\v2\x18.sentinel.v1.KeyLocationR\tlocations\x12'\n" + - "\x0fallow_anonymous\x18\x03 \x01(\bR\x0eallowAnonymous\x12)\n" + - "\x10permission_query\x18\x05 \x01(\tR\x0fpermissionQuery\"\xd6\x01\n" + + "\x19policies/v1/keyauth.proto\x12\vsentinel.v1\"\xaa\x01\n" + + "\aKeyAuth\x12\"\n" + + "\rkey_space_ids\x18\x01 \x03(\tR\vkeySpaceIds\x126\n" + + "\tlocations\x18\x02 \x03(\v2\x18.sentinel.v1.KeyLocationR\tlocations\x12.\n" + + "\x10permission_query\x18\x05 \x01(\tH\x00R\x0fpermissionQuery\x88\x01\x01B\x13\n" + + "\x11_permission_query\"\xd6\x01\n" + "\vKeyLocation\x12:\n" + "\x06bearer\x18\x01 \x01(\v2 .sentinel.v1.BearerTokenLocationH\x00R\x06bearer\x128\n" + "\x06header\x18\x02 \x01(\v2\x1e.sentinel.v1.HeaderKeyLocationH\x00R\x06header\x12E\n" + @@ -427,26 +412,26 @@ const file_middleware_v1_keyauth_proto_rawDesc = "" + "\x0fcom.sentinel.v1B\fKeyauthProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" var ( - file_middleware_v1_keyauth_proto_rawDescOnce sync.Once - file_middleware_v1_keyauth_proto_rawDescData []byte + file_policies_v1_keyauth_proto_rawDescOnce sync.Once + file_policies_v1_keyauth_proto_rawDescData []byte ) -func file_middleware_v1_keyauth_proto_rawDescGZIP() []byte { - file_middleware_v1_keyauth_proto_rawDescOnce.Do(func() { - file_middleware_v1_keyauth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_middleware_v1_keyauth_proto_rawDesc), len(file_middleware_v1_keyauth_proto_rawDesc))) +func file_policies_v1_keyauth_proto_rawDescGZIP() []byte { + file_policies_v1_keyauth_proto_rawDescOnce.Do(func() { + file_policies_v1_keyauth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_policies_v1_keyauth_proto_rawDesc), len(file_policies_v1_keyauth_proto_rawDesc))) }) - return file_middleware_v1_keyauth_proto_rawDescData + return file_policies_v1_keyauth_proto_rawDescData } -var file_middleware_v1_keyauth_proto_msgTypes = make([]protoimpl.MessageInfo, 5) -var file_middleware_v1_keyauth_proto_goTypes = []any{ +var file_policies_v1_keyauth_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_policies_v1_keyauth_proto_goTypes = []any{ (*KeyAuth)(nil), // 0: sentinel.v1.KeyAuth (*KeyLocation)(nil), // 1: sentinel.v1.KeyLocation (*BearerTokenLocation)(nil), // 2: sentinel.v1.BearerTokenLocation (*HeaderKeyLocation)(nil), // 3: sentinel.v1.HeaderKeyLocation (*QueryParamKeyLocation)(nil), // 4: sentinel.v1.QueryParamKeyLocation } -var file_middleware_v1_keyauth_proto_depIdxs = []int32{ +var file_policies_v1_keyauth_proto_depIdxs = []int32{ 1, // 0: sentinel.v1.KeyAuth.locations:type_name -> sentinel.v1.KeyLocation 2, // 1: sentinel.v1.KeyLocation.bearer:type_name -> sentinel.v1.BearerTokenLocation 3, // 2: sentinel.v1.KeyLocation.header:type_name -> sentinel.v1.HeaderKeyLocation @@ -458,12 +443,13 @@ var file_middleware_v1_keyauth_proto_depIdxs = []int32{ 0, // [0:4] is the sub-list for field type_name } -func init() { file_middleware_v1_keyauth_proto_init() } -func file_middleware_v1_keyauth_proto_init() { - if File_middleware_v1_keyauth_proto != nil { +func init() { file_policies_v1_keyauth_proto_init() } +func file_policies_v1_keyauth_proto_init() { + if File_policies_v1_keyauth_proto != nil { return } - file_middleware_v1_keyauth_proto_msgTypes[1].OneofWrappers = []any{ + file_policies_v1_keyauth_proto_msgTypes[0].OneofWrappers = []any{} + file_policies_v1_keyauth_proto_msgTypes[1].OneofWrappers = []any{ (*KeyLocation_Bearer)(nil), (*KeyLocation_Header)(nil), (*KeyLocation_QueryParam)(nil), @@ -472,17 +458,17 @@ func file_middleware_v1_keyauth_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_middleware_v1_keyauth_proto_rawDesc), len(file_middleware_v1_keyauth_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_policies_v1_keyauth_proto_rawDesc), len(file_policies_v1_keyauth_proto_rawDesc)), NumEnums: 0, NumMessages: 5, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_middleware_v1_keyauth_proto_goTypes, - DependencyIndexes: file_middleware_v1_keyauth_proto_depIdxs, - MessageInfos: file_middleware_v1_keyauth_proto_msgTypes, + GoTypes: file_policies_v1_keyauth_proto_goTypes, + DependencyIndexes: file_policies_v1_keyauth_proto_depIdxs, + MessageInfos: file_policies_v1_keyauth_proto_msgTypes, }.Build() - File_middleware_v1_keyauth_proto = out.File - file_middleware_v1_keyauth_proto_goTypes = nil - file_middleware_v1_keyauth_proto_depIdxs = nil + File_policies_v1_keyauth_proto = out.File + file_policies_v1_keyauth_proto_goTypes = nil + file_policies_v1_keyauth_proto_depIdxs = nil } diff --git a/gen/proto/sentinel/v1/match.pb.go b/gen/proto/sentinel/v1/match.pb.go index 6f475930f5..9e381b51cf 100644 --- a/gen/proto/sentinel/v1/match.pb.go +++ b/gen/proto/sentinel/v1/match.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.36.8 // protoc (unknown) -// source: middleware/v1/match.proto +// source: policies/v1/match.proto package sentinelv1 @@ -46,7 +46,7 @@ type MatchExpr struct { func (x *MatchExpr) Reset() { *x = MatchExpr{} - mi := &file_middleware_v1_match_proto_msgTypes[0] + mi := &file_policies_v1_match_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -58,7 +58,7 @@ func (x *MatchExpr) String() string { func (*MatchExpr) ProtoMessage() {} func (x *MatchExpr) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_match_proto_msgTypes[0] + mi := &file_policies_v1_match_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -71,7 +71,7 @@ func (x *MatchExpr) ProtoReflect() protoreflect.Message { // Deprecated: Use MatchExpr.ProtoReflect.Descriptor instead. func (*MatchExpr) Descriptor() ([]byte, []int) { - return file_middleware_v1_match_proto_rawDescGZIP(), []int{0} + return file_policies_v1_match_proto_rawDescGZIP(), []int{0} } func (x *MatchExpr) GetExpr() isMatchExpr_Expr { @@ -171,7 +171,7 @@ type StringMatch struct { func (x *StringMatch) Reset() { *x = StringMatch{} - mi := &file_middleware_v1_match_proto_msgTypes[1] + mi := &file_policies_v1_match_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -183,7 +183,7 @@ func (x *StringMatch) String() string { func (*StringMatch) ProtoMessage() {} func (x *StringMatch) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_match_proto_msgTypes[1] + mi := &file_policies_v1_match_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -196,7 +196,7 @@ func (x *StringMatch) ProtoReflect() protoreflect.Message { // Deprecated: Use StringMatch.ProtoReflect.Descriptor instead. func (*StringMatch) Descriptor() ([]byte, []int) { - return file_middleware_v1_match_proto_rawDescGZIP(), []int{1} + return file_policies_v1_match_proto_rawDescGZIP(), []int{1} } func (x *StringMatch) GetIgnoreCase() bool { @@ -281,7 +281,7 @@ type PathMatch struct { func (x *PathMatch) Reset() { *x = PathMatch{} - mi := &file_middleware_v1_match_proto_msgTypes[2] + mi := &file_policies_v1_match_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -293,7 +293,7 @@ func (x *PathMatch) String() string { func (*PathMatch) ProtoMessage() {} func (x *PathMatch) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_match_proto_msgTypes[2] + mi := &file_policies_v1_match_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -306,7 +306,7 @@ func (x *PathMatch) ProtoReflect() protoreflect.Message { // Deprecated: Use PathMatch.ProtoReflect.Descriptor instead. func (*PathMatch) Descriptor() ([]byte, []int) { - return file_middleware_v1_match_proto_rawDescGZIP(), []int{2} + return file_policies_v1_match_proto_rawDescGZIP(), []int{2} } func (x *PathMatch) GetPath() *StringMatch { @@ -331,7 +331,7 @@ type MethodMatch struct { func (x *MethodMatch) Reset() { *x = MethodMatch{} - mi := &file_middleware_v1_match_proto_msgTypes[3] + mi := &file_policies_v1_match_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -343,7 +343,7 @@ func (x *MethodMatch) String() string { func (*MethodMatch) ProtoMessage() {} func (x *MethodMatch) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_match_proto_msgTypes[3] + mi := &file_policies_v1_match_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -356,7 +356,7 @@ func (x *MethodMatch) ProtoReflect() protoreflect.Message { // Deprecated: Use MethodMatch.ProtoReflect.Descriptor instead. func (*MethodMatch) Descriptor() ([]byte, []int) { - return file_middleware_v1_match_proto_rawDescGZIP(), []int{3} + return file_policies_v1_match_proto_rawDescGZIP(), []int{3} } func (x *MethodMatch) GetMethods() []string { @@ -390,7 +390,7 @@ type HeaderMatch struct { func (x *HeaderMatch) Reset() { *x = HeaderMatch{} - mi := &file_middleware_v1_match_proto_msgTypes[4] + mi := &file_policies_v1_match_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -402,7 +402,7 @@ func (x *HeaderMatch) String() string { func (*HeaderMatch) ProtoMessage() {} func (x *HeaderMatch) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_match_proto_msgTypes[4] + mi := &file_policies_v1_match_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -415,7 +415,7 @@ func (x *HeaderMatch) ProtoReflect() protoreflect.Message { // Deprecated: Use HeaderMatch.ProtoReflect.Descriptor instead. func (*HeaderMatch) Descriptor() ([]byte, []int) { - return file_middleware_v1_match_proto_rawDescGZIP(), []int{4} + return file_policies_v1_match_proto_rawDescGZIP(), []int{4} } func (x *HeaderMatch) GetName() string { @@ -495,7 +495,7 @@ type QueryParamMatch struct { func (x *QueryParamMatch) Reset() { *x = QueryParamMatch{} - mi := &file_middleware_v1_match_proto_msgTypes[5] + mi := &file_policies_v1_match_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -507,7 +507,7 @@ func (x *QueryParamMatch) String() string { func (*QueryParamMatch) ProtoMessage() {} func (x *QueryParamMatch) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_match_proto_msgTypes[5] + mi := &file_policies_v1_match_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -520,7 +520,7 @@ func (x *QueryParamMatch) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryParamMatch.ProtoReflect.Descriptor instead. func (*QueryParamMatch) Descriptor() ([]byte, []int) { - return file_middleware_v1_match_proto_rawDescGZIP(), []int{5} + return file_policies_v1_match_proto_rawDescGZIP(), []int{5} } func (x *QueryParamMatch) GetName() string { @@ -575,11 +575,11 @@ func (*QueryParamMatch_Present) isQueryParamMatch_Match() {} func (*QueryParamMatch_Value) isQueryParamMatch_Match() {} -var File_middleware_v1_match_proto protoreflect.FileDescriptor +var File_policies_v1_match_proto protoreflect.FileDescriptor -const file_middleware_v1_match_proto_rawDesc = "" + +const file_policies_v1_match_proto_rawDesc = "" + "\n" + - "\x19middleware/v1/match.proto\x12\vsentinel.v1\"\xea\x01\n" + + "\x17policies/v1/match.proto\x12\vsentinel.v1\"\xea\x01\n" + "\tMatchExpr\x12,\n" + "\x04path\x18\x01 \x01(\v2\x16.sentinel.v1.PathMatchH\x00R\x04path\x122\n" + "\x06method\x18\x02 \x01(\v2\x18.sentinel.v1.MethodMatchH\x00R\x06method\x122\n" + @@ -612,19 +612,19 @@ const file_middleware_v1_match_proto_rawDesc = "" + "MatchProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" var ( - file_middleware_v1_match_proto_rawDescOnce sync.Once - file_middleware_v1_match_proto_rawDescData []byte + file_policies_v1_match_proto_rawDescOnce sync.Once + file_policies_v1_match_proto_rawDescData []byte ) -func file_middleware_v1_match_proto_rawDescGZIP() []byte { - file_middleware_v1_match_proto_rawDescOnce.Do(func() { - file_middleware_v1_match_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_middleware_v1_match_proto_rawDesc), len(file_middleware_v1_match_proto_rawDesc))) +func file_policies_v1_match_proto_rawDescGZIP() []byte { + file_policies_v1_match_proto_rawDescOnce.Do(func() { + file_policies_v1_match_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_policies_v1_match_proto_rawDesc), len(file_policies_v1_match_proto_rawDesc))) }) - return file_middleware_v1_match_proto_rawDescData + return file_policies_v1_match_proto_rawDescData } -var file_middleware_v1_match_proto_msgTypes = make([]protoimpl.MessageInfo, 6) -var file_middleware_v1_match_proto_goTypes = []any{ +var file_policies_v1_match_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_policies_v1_match_proto_goTypes = []any{ (*MatchExpr)(nil), // 0: sentinel.v1.MatchExpr (*StringMatch)(nil), // 1: sentinel.v1.StringMatch (*PathMatch)(nil), // 2: sentinel.v1.PathMatch @@ -632,7 +632,7 @@ var file_middleware_v1_match_proto_goTypes = []any{ (*HeaderMatch)(nil), // 4: sentinel.v1.HeaderMatch (*QueryParamMatch)(nil), // 5: sentinel.v1.QueryParamMatch } -var file_middleware_v1_match_proto_depIdxs = []int32{ +var file_policies_v1_match_proto_depIdxs = []int32{ 2, // 0: sentinel.v1.MatchExpr.path:type_name -> sentinel.v1.PathMatch 3, // 1: sentinel.v1.MatchExpr.method:type_name -> sentinel.v1.MethodMatch 4, // 2: sentinel.v1.MatchExpr.header:type_name -> sentinel.v1.HeaderMatch @@ -647,27 +647,27 @@ var file_middleware_v1_match_proto_depIdxs = []int32{ 0, // [0:7] is the sub-list for field type_name } -func init() { file_middleware_v1_match_proto_init() } -func file_middleware_v1_match_proto_init() { - if File_middleware_v1_match_proto != nil { +func init() { file_policies_v1_match_proto_init() } +func file_policies_v1_match_proto_init() { + if File_policies_v1_match_proto != nil { return } - file_middleware_v1_match_proto_msgTypes[0].OneofWrappers = []any{ + file_policies_v1_match_proto_msgTypes[0].OneofWrappers = []any{ (*MatchExpr_Path)(nil), (*MatchExpr_Method)(nil), (*MatchExpr_Header)(nil), (*MatchExpr_QueryParam)(nil), } - file_middleware_v1_match_proto_msgTypes[1].OneofWrappers = []any{ + file_policies_v1_match_proto_msgTypes[1].OneofWrappers = []any{ (*StringMatch_Exact)(nil), (*StringMatch_Prefix)(nil), (*StringMatch_Regex)(nil), } - file_middleware_v1_match_proto_msgTypes[4].OneofWrappers = []any{ + file_policies_v1_match_proto_msgTypes[4].OneofWrappers = []any{ (*HeaderMatch_Present)(nil), (*HeaderMatch_Value)(nil), } - file_middleware_v1_match_proto_msgTypes[5].OneofWrappers = []any{ + file_policies_v1_match_proto_msgTypes[5].OneofWrappers = []any{ (*QueryParamMatch_Present)(nil), (*QueryParamMatch_Value)(nil), } @@ -675,17 +675,17 @@ func file_middleware_v1_match_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_middleware_v1_match_proto_rawDesc), len(file_middleware_v1_match_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_policies_v1_match_proto_rawDesc), len(file_policies_v1_match_proto_rawDesc)), NumEnums: 0, NumMessages: 6, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_middleware_v1_match_proto_goTypes, - DependencyIndexes: file_middleware_v1_match_proto_depIdxs, - MessageInfos: file_middleware_v1_match_proto_msgTypes, + GoTypes: file_policies_v1_match_proto_goTypes, + DependencyIndexes: file_policies_v1_match_proto_depIdxs, + MessageInfos: file_policies_v1_match_proto_msgTypes, }.Build() - File_middleware_v1_match_proto = out.File - file_middleware_v1_match_proto_goTypes = nil - file_middleware_v1_match_proto_depIdxs = nil + File_policies_v1_match_proto = out.File + file_policies_v1_match_proto_goTypes = nil + file_policies_v1_match_proto_depIdxs = nil } diff --git a/gen/proto/sentinel/v1/middleware.pb.go b/gen/proto/sentinel/v1/middleware.pb.go deleted file mode 100644 index 736b9d1e53..0000000000 --- a/gen/proto/sentinel/v1/middleware.pb.go +++ /dev/null @@ -1,411 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.8 -// protoc (unknown) -// source: middleware/v1/middleware.proto - -package sentinelv1 - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// Middleware is the per-deployment policy configuration for sentinel. -// -// Sentinel is Unkey's reverse proxy. Each deployment gets a Middleware -// configuration that defines which policies apply to incoming requests and in -// what order. When a request arrives, sentinel evaluates every policy's -// match conditions against it, collects the matching policies, and executes -// them sequentially in list order. This gives operators full control over -// request processing without relying on implicit ordering conventions. -// -// A deployment with no policies is a plain pass-through proxy. Adding policies -// incrementally layers on authentication, authorization, traffic shaping, -// and validation — all without touching application code. -type Middleware struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The ordered list of policies for this deployment. Sentinel executes - // matching policies in exactly this order, so authn policies should appear - // before policies that depend on a [Principal]. - Policies []*Policy `protobuf:"bytes,1,rep,name=policies,proto3" json:"policies,omitempty"` - // CIDR ranges of trusted proxies sitting in front of sentinel, used to - // derive the real client IP from the X-Forwarded-For header chain. - // Sentinel walks X-Forwarded-For right-to-left, skipping entries that - // fall within a trusted CIDR, and uses the first untrusted entry as the - // client IP. When this list is empty, sentinel uses the direct peer IP - // and ignores X-Forwarded-For entirely — this is the safe default that - // prevents IP spoofing via forged headers. - // - // This setting affects all policies that depend on client IP: [IPRules] - // for allow/deny decisions and [RateLimit] with a [RemoteIpKey] source. - // - // Examples: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - TrustedProxyCidrs []string `protobuf:"bytes,2,rep,name=trusted_proxy_cidrs,json=trustedProxyCidrs,proto3" json:"trusted_proxy_cidrs,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Middleware) Reset() { - *x = Middleware{} - mi := &file_middleware_v1_middleware_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Middleware) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Middleware) ProtoMessage() {} - -func (x *Middleware) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_middleware_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Middleware.ProtoReflect.Descriptor instead. -func (*Middleware) Descriptor() ([]byte, []int) { - return file_middleware_v1_middleware_proto_rawDescGZIP(), []int{0} -} - -func (x *Middleware) GetPolicies() []*Policy { - if x != nil { - return x.Policies - } - return nil -} - -func (x *Middleware) GetTrustedProxyCidrs() []string { - if x != nil { - return x.TrustedProxyCidrs - } - return nil -} - -// Policy is a single middleware layer in a deployment's configuration. Each policy -// combines a match expression (which requests does it apply to?) with a -// configuration (what does it do?). This separation is what makes the system -// composable: the same rate limiter config can be scoped to POST /api/* -// without the rate limiter needing to know anything about path matching. -// -// Policies carry a stable id for correlation across logs, metrics, and -// debugging. The disabled flag allows operators to disable a policy without -// removing it from config, which is critical for incident response — you can -// turn off a misbehaving policy and re-enable it once the issue is resolved, -// without losing the configuration or triggering a full redeploy. -type Policy struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Stable identifier for this policy, used in log entries, metrics labels, - // and error messages. Should be unique within a deployment's Middleware - // config. Typically a UUID or a slug like "api-ratelimit". - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // Human-friendly label displayed in the dashboard and audit logs. - // Does not affect policy behavior. - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - // When false, sentinel skips this policy entirely during evaluation. - // This allows operators to toggle policies on and off without modifying - // or removing the underlying configuration, which is useful during - // incidents, gradual rollouts, and debugging. - Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` - // Match conditions that determine which requests this policy applies to. - // All entries must match for the policy to run (implicit AND). An empty - // list matches all requests — this is the common case for global policies - // like IP allowlists or rate limiting. - // - // For OR semantics, create separate policies with the same config and - // different match lists. - Match []*MatchExpr `protobuf:"bytes,4,rep,name=match,proto3" json:"match,omitempty"` - // The policy configuration. Exactly one must be set. - // - // Types that are valid to be assigned to Config: - // - // *Policy_Keyauth - // *Policy_Jwtauth - // *Policy_Basicauth - // *Policy_Ratelimit - // *Policy_IpRules - // *Policy_Openapi - Config isPolicy_Config `protobuf_oneof:"config"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Policy) Reset() { - *x = Policy{} - mi := &file_middleware_v1_middleware_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Policy) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Policy) ProtoMessage() {} - -func (x *Policy) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_middleware_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Policy.ProtoReflect.Descriptor instead. -func (*Policy) Descriptor() ([]byte, []int) { - return file_middleware_v1_middleware_proto_rawDescGZIP(), []int{1} -} - -func (x *Policy) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *Policy) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *Policy) GetEnabled() bool { - if x != nil { - return x.Enabled - } - return false -} - -func (x *Policy) GetMatch() []*MatchExpr { - if x != nil { - return x.Match - } - return nil -} - -func (x *Policy) GetConfig() isPolicy_Config { - if x != nil { - return x.Config - } - return nil -} - -func (x *Policy) GetKeyauth() *KeyAuth { - if x != nil { - if x, ok := x.Config.(*Policy_Keyauth); ok { - return x.Keyauth - } - } - return nil -} - -func (x *Policy) GetJwtauth() *JWTAuth { - if x != nil { - if x, ok := x.Config.(*Policy_Jwtauth); ok { - return x.Jwtauth - } - } - return nil -} - -func (x *Policy) GetBasicauth() *BasicAuth { - if x != nil { - if x, ok := x.Config.(*Policy_Basicauth); ok { - return x.Basicauth - } - } - return nil -} - -func (x *Policy) GetRatelimit() *RateLimit { - if x != nil { - if x, ok := x.Config.(*Policy_Ratelimit); ok { - return x.Ratelimit - } - } - return nil -} - -func (x *Policy) GetIpRules() *IPRules { - if x != nil { - if x, ok := x.Config.(*Policy_IpRules); ok { - return x.IpRules - } - } - return nil -} - -func (x *Policy) GetOpenapi() *OpenApiRequestValidation { - if x != nil { - if x, ok := x.Config.(*Policy_Openapi); ok { - return x.Openapi - } - } - return nil -} - -type isPolicy_Config interface { - isPolicy_Config() -} - -type Policy_Keyauth struct { - Keyauth *KeyAuth `protobuf:"bytes,5,opt,name=keyauth,proto3,oneof"` -} - -type Policy_Jwtauth struct { - Jwtauth *JWTAuth `protobuf:"bytes,6,opt,name=jwtauth,proto3,oneof"` -} - -type Policy_Basicauth struct { - Basicauth *BasicAuth `protobuf:"bytes,7,opt,name=basicauth,proto3,oneof"` -} - -type Policy_Ratelimit struct { - Ratelimit *RateLimit `protobuf:"bytes,8,opt,name=ratelimit,proto3,oneof"` -} - -type Policy_IpRules struct { - IpRules *IPRules `protobuf:"bytes,9,opt,name=ip_rules,json=ipRules,proto3,oneof"` -} - -type Policy_Openapi struct { - Openapi *OpenApiRequestValidation `protobuf:"bytes,10,opt,name=openapi,proto3,oneof"` -} - -func (*Policy_Keyauth) isPolicy_Config() {} - -func (*Policy_Jwtauth) isPolicy_Config() {} - -func (*Policy_Basicauth) isPolicy_Config() {} - -func (*Policy_Ratelimit) isPolicy_Config() {} - -func (*Policy_IpRules) isPolicy_Config() {} - -func (*Policy_Openapi) isPolicy_Config() {} - -var File_middleware_v1_middleware_proto protoreflect.FileDescriptor - -const file_middleware_v1_middleware_proto_rawDesc = "" + - "\n" + - "\x1emiddleware/v1/middleware.proto\x12\vsentinel.v1\x1a\x1dmiddleware/v1/basicauth.proto\x1a\x1bmiddleware/v1/iprules.proto\x1a\x1bmiddleware/v1/jwtauth.proto\x1a\x1bmiddleware/v1/keyauth.proto\x1a\x19middleware/v1/match.proto\x1a\x1bmiddleware/v1/openapi.proto\x1a\x1dmiddleware/v1/ratelimit.proto\"m\n" + - "\n" + - "Middleware\x12/\n" + - "\bpolicies\x18\x01 \x03(\v2\x13.sentinel.v1.PolicyR\bpolicies\x12.\n" + - "\x13trusted_proxy_cidrs\x18\x02 \x03(\tR\x11trustedProxyCidrs\"\xc8\x03\n" + - "\x06Policy\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + - "\aenabled\x18\x03 \x01(\bR\aenabled\x12,\n" + - "\x05match\x18\x04 \x03(\v2\x16.sentinel.v1.MatchExprR\x05match\x120\n" + - "\akeyauth\x18\x05 \x01(\v2\x14.sentinel.v1.KeyAuthH\x00R\akeyauth\x120\n" + - "\ajwtauth\x18\x06 \x01(\v2\x14.sentinel.v1.JWTAuthH\x00R\ajwtauth\x126\n" + - "\tbasicauth\x18\a \x01(\v2\x16.sentinel.v1.BasicAuthH\x00R\tbasicauth\x126\n" + - "\tratelimit\x18\b \x01(\v2\x16.sentinel.v1.RateLimitH\x00R\tratelimit\x121\n" + - "\bip_rules\x18\t \x01(\v2\x14.sentinel.v1.IPRulesH\x00R\aipRules\x12A\n" + - "\aopenapi\x18\n" + - " \x01(\v2%.sentinel.v1.OpenApiRequestValidationH\x00R\aopenapiB\b\n" + - "\x06configB\xaa\x01\n" + - "\x0fcom.sentinel.v1B\x0fMiddlewareProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" - -var ( - file_middleware_v1_middleware_proto_rawDescOnce sync.Once - file_middleware_v1_middleware_proto_rawDescData []byte -) - -func file_middleware_v1_middleware_proto_rawDescGZIP() []byte { - file_middleware_v1_middleware_proto_rawDescOnce.Do(func() { - file_middleware_v1_middleware_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_middleware_v1_middleware_proto_rawDesc), len(file_middleware_v1_middleware_proto_rawDesc))) - }) - return file_middleware_v1_middleware_proto_rawDescData -} - -var file_middleware_v1_middleware_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_middleware_v1_middleware_proto_goTypes = []any{ - (*Middleware)(nil), // 0: sentinel.v1.Middleware - (*Policy)(nil), // 1: sentinel.v1.Policy - (*MatchExpr)(nil), // 2: sentinel.v1.MatchExpr - (*KeyAuth)(nil), // 3: sentinel.v1.KeyAuth - (*JWTAuth)(nil), // 4: sentinel.v1.JWTAuth - (*BasicAuth)(nil), // 5: sentinel.v1.BasicAuth - (*RateLimit)(nil), // 6: sentinel.v1.RateLimit - (*IPRules)(nil), // 7: sentinel.v1.IPRules - (*OpenApiRequestValidation)(nil), // 8: sentinel.v1.OpenApiRequestValidation -} -var file_middleware_v1_middleware_proto_depIdxs = []int32{ - 1, // 0: sentinel.v1.Middleware.policies:type_name -> sentinel.v1.Policy - 2, // 1: sentinel.v1.Policy.match:type_name -> sentinel.v1.MatchExpr - 3, // 2: sentinel.v1.Policy.keyauth:type_name -> sentinel.v1.KeyAuth - 4, // 3: sentinel.v1.Policy.jwtauth:type_name -> sentinel.v1.JWTAuth - 5, // 4: sentinel.v1.Policy.basicauth:type_name -> sentinel.v1.BasicAuth - 6, // 5: sentinel.v1.Policy.ratelimit:type_name -> sentinel.v1.RateLimit - 7, // 6: sentinel.v1.Policy.ip_rules:type_name -> sentinel.v1.IPRules - 8, // 7: sentinel.v1.Policy.openapi:type_name -> sentinel.v1.OpenApiRequestValidation - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name -} - -func init() { file_middleware_v1_middleware_proto_init() } -func file_middleware_v1_middleware_proto_init() { - if File_middleware_v1_middleware_proto != nil { - return - } - file_middleware_v1_basicauth_proto_init() - file_middleware_v1_iprules_proto_init() - file_middleware_v1_jwtauth_proto_init() - file_middleware_v1_keyauth_proto_init() - file_middleware_v1_match_proto_init() - file_middleware_v1_openapi_proto_init() - file_middleware_v1_ratelimit_proto_init() - file_middleware_v1_middleware_proto_msgTypes[1].OneofWrappers = []any{ - (*Policy_Keyauth)(nil), - (*Policy_Jwtauth)(nil), - (*Policy_Basicauth)(nil), - (*Policy_Ratelimit)(nil), - (*Policy_IpRules)(nil), - (*Policy_Openapi)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_middleware_v1_middleware_proto_rawDesc), len(file_middleware_v1_middleware_proto_rawDesc)), - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_middleware_v1_middleware_proto_goTypes, - DependencyIndexes: file_middleware_v1_middleware_proto_depIdxs, - MessageInfos: file_middleware_v1_middleware_proto_msgTypes, - }.Build() - File_middleware_v1_middleware_proto = out.File - file_middleware_v1_middleware_proto_goTypes = nil - file_middleware_v1_middleware_proto_depIdxs = nil -} diff --git a/gen/proto/sentinel/v1/openapi.pb.go b/gen/proto/sentinel/v1/openapi.pb.go index 9b511b346c..ea8eebbb01 100644 --- a/gen/proto/sentinel/v1/openapi.pb.go +++ b/gen/proto/sentinel/v1/openapi.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.36.8 // protoc (unknown) -// source: middleware/v1/openapi.proto +// source: policies/v1/openapi.proto package sentinelv1 @@ -55,7 +55,7 @@ type OpenApiRequestValidation struct { func (x *OpenApiRequestValidation) Reset() { *x = OpenApiRequestValidation{} - mi := &file_middleware_v1_openapi_proto_msgTypes[0] + mi := &file_policies_v1_openapi_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -67,7 +67,7 @@ func (x *OpenApiRequestValidation) String() string { func (*OpenApiRequestValidation) ProtoMessage() {} func (x *OpenApiRequestValidation) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_openapi_proto_msgTypes[0] + mi := &file_policies_v1_openapi_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -80,7 +80,7 @@ func (x *OpenApiRequestValidation) ProtoReflect() protoreflect.Message { // Deprecated: Use OpenApiRequestValidation.ProtoReflect.Descriptor instead. func (*OpenApiRequestValidation) Descriptor() ([]byte, []int) { - return file_middleware_v1_openapi_proto_rawDescGZIP(), []int{0} + return file_policies_v1_openapi_proto_rawDescGZIP(), []int{0} } func (x *OpenApiRequestValidation) GetSpecYaml() []byte { @@ -90,32 +90,32 @@ func (x *OpenApiRequestValidation) GetSpecYaml() []byte { return nil } -var File_middleware_v1_openapi_proto protoreflect.FileDescriptor +var File_policies_v1_openapi_proto protoreflect.FileDescriptor -const file_middleware_v1_openapi_proto_rawDesc = "" + +const file_policies_v1_openapi_proto_rawDesc = "" + "\n" + - "\x1bmiddleware/v1/openapi.proto\x12\vsentinel.v1\"7\n" + + "\x19policies/v1/openapi.proto\x12\vsentinel.v1\"7\n" + "\x18OpenApiRequestValidation\x12\x1b\n" + "\tspec_yaml\x18\x01 \x01(\fR\bspecYamlB\xa7\x01\n" + "\x0fcom.sentinel.v1B\fOpenapiProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" var ( - file_middleware_v1_openapi_proto_rawDescOnce sync.Once - file_middleware_v1_openapi_proto_rawDescData []byte + file_policies_v1_openapi_proto_rawDescOnce sync.Once + file_policies_v1_openapi_proto_rawDescData []byte ) -func file_middleware_v1_openapi_proto_rawDescGZIP() []byte { - file_middleware_v1_openapi_proto_rawDescOnce.Do(func() { - file_middleware_v1_openapi_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_middleware_v1_openapi_proto_rawDesc), len(file_middleware_v1_openapi_proto_rawDesc))) +func file_policies_v1_openapi_proto_rawDescGZIP() []byte { + file_policies_v1_openapi_proto_rawDescOnce.Do(func() { + file_policies_v1_openapi_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_policies_v1_openapi_proto_rawDesc), len(file_policies_v1_openapi_proto_rawDesc))) }) - return file_middleware_v1_openapi_proto_rawDescData + return file_policies_v1_openapi_proto_rawDescData } -var file_middleware_v1_openapi_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_middleware_v1_openapi_proto_goTypes = []any{ +var file_policies_v1_openapi_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_policies_v1_openapi_proto_goTypes = []any{ (*OpenApiRequestValidation)(nil), // 0: sentinel.v1.OpenApiRequestValidation } -var file_middleware_v1_openapi_proto_depIdxs = []int32{ +var file_policies_v1_openapi_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name @@ -123,26 +123,26 @@ var file_middleware_v1_openapi_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for field type_name } -func init() { file_middleware_v1_openapi_proto_init() } -func file_middleware_v1_openapi_proto_init() { - if File_middleware_v1_openapi_proto != nil { +func init() { file_policies_v1_openapi_proto_init() } +func file_policies_v1_openapi_proto_init() { + if File_policies_v1_openapi_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_middleware_v1_openapi_proto_rawDesc), len(file_middleware_v1_openapi_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_policies_v1_openapi_proto_rawDesc), len(file_policies_v1_openapi_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_middleware_v1_openapi_proto_goTypes, - DependencyIndexes: file_middleware_v1_openapi_proto_depIdxs, - MessageInfos: file_middleware_v1_openapi_proto_msgTypes, + GoTypes: file_policies_v1_openapi_proto_goTypes, + DependencyIndexes: file_policies_v1_openapi_proto_depIdxs, + MessageInfos: file_policies_v1_openapi_proto_msgTypes, }.Build() - File_middleware_v1_openapi_proto = out.File - file_middleware_v1_openapi_proto_goTypes = nil - file_middleware_v1_openapi_proto_depIdxs = nil + File_policies_v1_openapi_proto = out.File + file_policies_v1_openapi_proto_goTypes = nil + file_policies_v1_openapi_proto_depIdxs = nil } diff --git a/gen/proto/sentinel/v1/policy.pb.go b/gen/proto/sentinel/v1/policy.pb.go new file mode 100644 index 0000000000..a1d4bfae6d --- /dev/null +++ b/gen/proto/sentinel/v1/policy.pb.go @@ -0,0 +1,326 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc (unknown) +// source: policies/v1/policy.proto + +package sentinelv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Policy is a single middleware layer in a deployment's configuration. Each policy +// combines a match expression (which requests does it apply to?) with a +// configuration (what does it do?). This separation is what makes the system +// composable: the same rate limiter config can be scoped to POST /api/* +// without the rate limiter needing to know anything about path matching. +// +// Policies carry a stable id for correlation across logs, metrics, and +// debugging. The disabled flag allows operators to disable a policy without +// removing it from config, which is critical for incident response — you can +// turn off a misbehaving policy and re-enable it once the issue is resolved, +// without losing the configuration or triggering a full redeploy. +type Policy struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable identifier for this policy, used in log entries, metrics labels, + // and error messages. Should be unique within a deployment's Middleware + // config. Typically a UUID or a slug like "api-ratelimit". + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Human-friendly label displayed in the dashboard and audit logs. + // Does not affect policy behavior. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // When false, sentinel skips this policy entirely during evaluation. + // This allows operators to toggle policies on and off without modifying + // or removing the underlying configuration, which is useful during + // incidents, gradual rollouts, and debugging. + Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` + // Match conditions that determine which requests this policy applies to. + // All entries must match for the policy to run (implicit AND). An empty + // list matches all requests — this is the common case for global policies + // like IP allowlists or rate limiting. + // + // For OR semantics, create separate policies with the same config and + // different match lists. + Match []*MatchExpr `protobuf:"bytes,4,rep,name=match,proto3" json:"match,omitempty"` + // The policy configuration. Exactly one must be set. + // + // Types that are valid to be assigned to Config: + // + // *Policy_Keyauth + // *Policy_Jwtauth + // *Policy_Basicauth + // *Policy_Ratelimit + // *Policy_IpRules + // *Policy_Openapi + Config isPolicy_Config `protobuf_oneof:"config"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Policy) Reset() { + *x = Policy{} + mi := &file_policies_v1_policy_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Policy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Policy) ProtoMessage() {} + +func (x *Policy) ProtoReflect() protoreflect.Message { + mi := &file_policies_v1_policy_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Policy.ProtoReflect.Descriptor instead. +func (*Policy) Descriptor() ([]byte, []int) { + return file_policies_v1_policy_proto_rawDescGZIP(), []int{0} +} + +func (x *Policy) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Policy) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Policy) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *Policy) GetMatch() []*MatchExpr { + if x != nil { + return x.Match + } + return nil +} + +func (x *Policy) GetConfig() isPolicy_Config { + if x != nil { + return x.Config + } + return nil +} + +func (x *Policy) GetKeyauth() *KeyAuth { + if x != nil { + if x, ok := x.Config.(*Policy_Keyauth); ok { + return x.Keyauth + } + } + return nil +} + +func (x *Policy) GetJwtauth() *JWTAuth { + if x != nil { + if x, ok := x.Config.(*Policy_Jwtauth); ok { + return x.Jwtauth + } + } + return nil +} + +func (x *Policy) GetBasicauth() *BasicAuth { + if x != nil { + if x, ok := x.Config.(*Policy_Basicauth); ok { + return x.Basicauth + } + } + return nil +} + +func (x *Policy) GetRatelimit() *RateLimit { + if x != nil { + if x, ok := x.Config.(*Policy_Ratelimit); ok { + return x.Ratelimit + } + } + return nil +} + +func (x *Policy) GetIpRules() *IPRules { + if x != nil { + if x, ok := x.Config.(*Policy_IpRules); ok { + return x.IpRules + } + } + return nil +} + +func (x *Policy) GetOpenapi() *OpenApiRequestValidation { + if x != nil { + if x, ok := x.Config.(*Policy_Openapi); ok { + return x.Openapi + } + } + return nil +} + +type isPolicy_Config interface { + isPolicy_Config() +} + +type Policy_Keyauth struct { + Keyauth *KeyAuth `protobuf:"bytes,5,opt,name=keyauth,proto3,oneof"` +} + +type Policy_Jwtauth struct { + Jwtauth *JWTAuth `protobuf:"bytes,6,opt,name=jwtauth,proto3,oneof"` +} + +type Policy_Basicauth struct { + Basicauth *BasicAuth `protobuf:"bytes,7,opt,name=basicauth,proto3,oneof"` +} + +type Policy_Ratelimit struct { + Ratelimit *RateLimit `protobuf:"bytes,8,opt,name=ratelimit,proto3,oneof"` +} + +type Policy_IpRules struct { + IpRules *IPRules `protobuf:"bytes,9,opt,name=ip_rules,json=ipRules,proto3,oneof"` +} + +type Policy_Openapi struct { + Openapi *OpenApiRequestValidation `protobuf:"bytes,10,opt,name=openapi,proto3,oneof"` +} + +func (*Policy_Keyauth) isPolicy_Config() {} + +func (*Policy_Jwtauth) isPolicy_Config() {} + +func (*Policy_Basicauth) isPolicy_Config() {} + +func (*Policy_Ratelimit) isPolicy_Config() {} + +func (*Policy_IpRules) isPolicy_Config() {} + +func (*Policy_Openapi) isPolicy_Config() {} + +var File_policies_v1_policy_proto protoreflect.FileDescriptor + +const file_policies_v1_policy_proto_rawDesc = "" + + "\n" + + "\x18policies/v1/policy.proto\x12\vsentinel.v1\x1a\x1bpolicies/v1/basicauth.proto\x1a\x19policies/v1/iprules.proto\x1a\x19policies/v1/jwtauth.proto\x1a\x19policies/v1/keyauth.proto\x1a\x17policies/v1/match.proto\x1a\x19policies/v1/openapi.proto\x1a\x1bpolicies/v1/ratelimit.proto\"\xc8\x03\n" + + "\x06Policy\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + + "\aenabled\x18\x03 \x01(\bR\aenabled\x12,\n" + + "\x05match\x18\x04 \x03(\v2\x16.sentinel.v1.MatchExprR\x05match\x120\n" + + "\akeyauth\x18\x05 \x01(\v2\x14.sentinel.v1.KeyAuthH\x00R\akeyauth\x120\n" + + "\ajwtauth\x18\x06 \x01(\v2\x14.sentinel.v1.JWTAuthH\x00R\ajwtauth\x126\n" + + "\tbasicauth\x18\a \x01(\v2\x16.sentinel.v1.BasicAuthH\x00R\tbasicauth\x126\n" + + "\tratelimit\x18\b \x01(\v2\x16.sentinel.v1.RateLimitH\x00R\tratelimit\x121\n" + + "\bip_rules\x18\t \x01(\v2\x14.sentinel.v1.IPRulesH\x00R\aipRules\x12A\n" + + "\aopenapi\x18\n" + + " \x01(\v2%.sentinel.v1.OpenApiRequestValidationH\x00R\aopenapiB\b\n" + + "\x06configB\xa6\x01\n" + + "\x0fcom.sentinel.v1B\vPolicyProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" + +var ( + file_policies_v1_policy_proto_rawDescOnce sync.Once + file_policies_v1_policy_proto_rawDescData []byte +) + +func file_policies_v1_policy_proto_rawDescGZIP() []byte { + file_policies_v1_policy_proto_rawDescOnce.Do(func() { + file_policies_v1_policy_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_policies_v1_policy_proto_rawDesc), len(file_policies_v1_policy_proto_rawDesc))) + }) + return file_policies_v1_policy_proto_rawDescData +} + +var file_policies_v1_policy_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_policies_v1_policy_proto_goTypes = []any{ + (*Policy)(nil), // 0: sentinel.v1.Policy + (*MatchExpr)(nil), // 1: sentinel.v1.MatchExpr + (*KeyAuth)(nil), // 2: sentinel.v1.KeyAuth + (*JWTAuth)(nil), // 3: sentinel.v1.JWTAuth + (*BasicAuth)(nil), // 4: sentinel.v1.BasicAuth + (*RateLimit)(nil), // 5: sentinel.v1.RateLimit + (*IPRules)(nil), // 6: sentinel.v1.IPRules + (*OpenApiRequestValidation)(nil), // 7: sentinel.v1.OpenApiRequestValidation +} +var file_policies_v1_policy_proto_depIdxs = []int32{ + 1, // 0: sentinel.v1.Policy.match:type_name -> sentinel.v1.MatchExpr + 2, // 1: sentinel.v1.Policy.keyauth:type_name -> sentinel.v1.KeyAuth + 3, // 2: sentinel.v1.Policy.jwtauth:type_name -> sentinel.v1.JWTAuth + 4, // 3: sentinel.v1.Policy.basicauth:type_name -> sentinel.v1.BasicAuth + 5, // 4: sentinel.v1.Policy.ratelimit:type_name -> sentinel.v1.RateLimit + 6, // 5: sentinel.v1.Policy.ip_rules:type_name -> sentinel.v1.IPRules + 7, // 6: sentinel.v1.Policy.openapi:type_name -> sentinel.v1.OpenApiRequestValidation + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_policies_v1_policy_proto_init() } +func file_policies_v1_policy_proto_init() { + if File_policies_v1_policy_proto != nil { + return + } + file_policies_v1_basicauth_proto_init() + file_policies_v1_iprules_proto_init() + file_policies_v1_jwtauth_proto_init() + file_policies_v1_keyauth_proto_init() + file_policies_v1_match_proto_init() + file_policies_v1_openapi_proto_init() + file_policies_v1_ratelimit_proto_init() + file_policies_v1_policy_proto_msgTypes[0].OneofWrappers = []any{ + (*Policy_Keyauth)(nil), + (*Policy_Jwtauth)(nil), + (*Policy_Basicauth)(nil), + (*Policy_Ratelimit)(nil), + (*Policy_IpRules)(nil), + (*Policy_Openapi)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_policies_v1_policy_proto_rawDesc), len(file_policies_v1_policy_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_policies_v1_policy_proto_goTypes, + DependencyIndexes: file_policies_v1_policy_proto_depIdxs, + MessageInfos: file_policies_v1_policy_proto_msgTypes, + }.Build() + File_policies_v1_policy_proto = out.File + file_policies_v1_policy_proto_goTypes = nil + file_policies_v1_policy_proto_depIdxs = nil +} diff --git a/gen/proto/sentinel/v1/principal.pb.go b/gen/proto/sentinel/v1/principal.pb.go index b2569bd21f..4033e3f670 100644 --- a/gen/proto/sentinel/v1/principal.pb.go +++ b/gen/proto/sentinel/v1/principal.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.36.8 // protoc (unknown) -// source: middleware/v1/principal.proto +// source: policies/v1/principal.proto package sentinelv1 @@ -62,11 +62,11 @@ func (x PrincipalType) String() string { } func (PrincipalType) Descriptor() protoreflect.EnumDescriptor { - return file_middleware_v1_principal_proto_enumTypes[0].Descriptor() + return file_policies_v1_principal_proto_enumTypes[0].Descriptor() } func (PrincipalType) Type() protoreflect.EnumType { - return &file_middleware_v1_principal_proto_enumTypes[0] + return &file_policies_v1_principal_proto_enumTypes[0] } func (x PrincipalType) Number() protoreflect.EnumNumber { @@ -75,7 +75,7 @@ func (x PrincipalType) Number() protoreflect.EnumNumber { // Deprecated: Use PrincipalType.Descriptor instead. func (PrincipalType) EnumDescriptor() ([]byte, []int) { - return file_middleware_v1_principal_proto_rawDescGZIP(), []int{0} + return file_policies_v1_principal_proto_rawDescGZIP(), []int{0} } // Principal is the authenticated entity produced by any authentication policy. @@ -129,7 +129,7 @@ type Principal struct { func (x *Principal) Reset() { *x = Principal{} - mi := &file_middleware_v1_principal_proto_msgTypes[0] + mi := &file_policies_v1_principal_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -141,7 +141,7 @@ func (x *Principal) String() string { func (*Principal) ProtoMessage() {} func (x *Principal) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_principal_proto_msgTypes[0] + mi := &file_policies_v1_principal_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -154,7 +154,7 @@ func (x *Principal) ProtoReflect() protoreflect.Message { // Deprecated: Use Principal.ProtoReflect.Descriptor instead. func (*Principal) Descriptor() ([]byte, []int) { - return file_middleware_v1_principal_proto_rawDescGZIP(), []int{0} + return file_policies_v1_principal_proto_rawDescGZIP(), []int{0} } func (x *Principal) GetSubject() string { @@ -178,11 +178,11 @@ func (x *Principal) GetClaims() map[string]string { return nil } -var File_middleware_v1_principal_proto protoreflect.FileDescriptor +var File_policies_v1_principal_proto protoreflect.FileDescriptor -const file_middleware_v1_principal_proto_rawDesc = "" + +const file_policies_v1_principal_proto_rawDesc = "" + "\n" + - "\x1dmiddleware/v1/principal.proto\x12\vsentinel.v1\"\xcc\x01\n" + + "\x1bpolicies/v1/principal.proto\x12\vsentinel.v1\"\xcc\x01\n" + "\tPrincipal\x12\x18\n" + "\asubject\x18\x01 \x01(\tR\asubject\x12.\n" + "\x04type\x18\x02 \x01(\x0e2\x1a.sentinel.v1.PrincipalTypeR\x04type\x12:\n" + @@ -198,25 +198,25 @@ const file_middleware_v1_principal_proto_rawDesc = "" + "\x0fcom.sentinel.v1B\x0ePrincipalProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" var ( - file_middleware_v1_principal_proto_rawDescOnce sync.Once - file_middleware_v1_principal_proto_rawDescData []byte + file_policies_v1_principal_proto_rawDescOnce sync.Once + file_policies_v1_principal_proto_rawDescData []byte ) -func file_middleware_v1_principal_proto_rawDescGZIP() []byte { - file_middleware_v1_principal_proto_rawDescOnce.Do(func() { - file_middleware_v1_principal_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_middleware_v1_principal_proto_rawDesc), len(file_middleware_v1_principal_proto_rawDesc))) +func file_policies_v1_principal_proto_rawDescGZIP() []byte { + file_policies_v1_principal_proto_rawDescOnce.Do(func() { + file_policies_v1_principal_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_policies_v1_principal_proto_rawDesc), len(file_policies_v1_principal_proto_rawDesc))) }) - return file_middleware_v1_principal_proto_rawDescData + return file_policies_v1_principal_proto_rawDescData } -var file_middleware_v1_principal_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_middleware_v1_principal_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_middleware_v1_principal_proto_goTypes = []any{ +var file_policies_v1_principal_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_policies_v1_principal_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_policies_v1_principal_proto_goTypes = []any{ (PrincipalType)(0), // 0: sentinel.v1.PrincipalType (*Principal)(nil), // 1: sentinel.v1.Principal nil, // 2: sentinel.v1.Principal.ClaimsEntry } -var file_middleware_v1_principal_proto_depIdxs = []int32{ +var file_policies_v1_principal_proto_depIdxs = []int32{ 0, // 0: sentinel.v1.Principal.type:type_name -> sentinel.v1.PrincipalType 2, // 1: sentinel.v1.Principal.claims:type_name -> sentinel.v1.Principal.ClaimsEntry 2, // [2:2] is the sub-list for method output_type @@ -226,27 +226,27 @@ var file_middleware_v1_principal_proto_depIdxs = []int32{ 0, // [0:2] is the sub-list for field type_name } -func init() { file_middleware_v1_principal_proto_init() } -func file_middleware_v1_principal_proto_init() { - if File_middleware_v1_principal_proto != nil { +func init() { file_policies_v1_principal_proto_init() } +func file_policies_v1_principal_proto_init() { + if File_policies_v1_principal_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_middleware_v1_principal_proto_rawDesc), len(file_middleware_v1_principal_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_policies_v1_principal_proto_rawDesc), len(file_policies_v1_principal_proto_rawDesc)), NumEnums: 1, NumMessages: 2, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_middleware_v1_principal_proto_goTypes, - DependencyIndexes: file_middleware_v1_principal_proto_depIdxs, - EnumInfos: file_middleware_v1_principal_proto_enumTypes, - MessageInfos: file_middleware_v1_principal_proto_msgTypes, + GoTypes: file_policies_v1_principal_proto_goTypes, + DependencyIndexes: file_policies_v1_principal_proto_depIdxs, + EnumInfos: file_policies_v1_principal_proto_enumTypes, + MessageInfos: file_policies_v1_principal_proto_msgTypes, }.Build() - File_middleware_v1_principal_proto = out.File - file_middleware_v1_principal_proto_goTypes = nil - file_middleware_v1_principal_proto_depIdxs = nil + File_policies_v1_principal_proto = out.File + file_policies_v1_principal_proto_goTypes = nil + file_policies_v1_principal_proto_depIdxs = nil } diff --git a/gen/proto/sentinel/v1/ratelimit.pb.go b/gen/proto/sentinel/v1/ratelimit.pb.go index f96ed3ade1..6705ec3722 100644 --- a/gen/proto/sentinel/v1/ratelimit.pb.go +++ b/gen/proto/sentinel/v1/ratelimit.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.36.8 // protoc (unknown) -// source: middleware/v1/ratelimit.proto +// source: policies/v1/ratelimit.proto package sentinelv1 @@ -57,7 +57,7 @@ type RateLimit struct { func (x *RateLimit) Reset() { *x = RateLimit{} - mi := &file_middleware_v1_ratelimit_proto_msgTypes[0] + mi := &file_policies_v1_ratelimit_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -69,7 +69,7 @@ func (x *RateLimit) String() string { func (*RateLimit) ProtoMessage() {} func (x *RateLimit) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_ratelimit_proto_msgTypes[0] + mi := &file_policies_v1_ratelimit_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -82,7 +82,7 @@ func (x *RateLimit) ProtoReflect() protoreflect.Message { // Deprecated: Use RateLimit.ProtoReflect.Descriptor instead. func (*RateLimit) Descriptor() ([]byte, []int) { - return file_middleware_v1_ratelimit_proto_rawDescGZIP(), []int{0} + return file_policies_v1_ratelimit_proto_rawDescGZIP(), []int{0} } func (x *RateLimit) GetLimit() int64 { @@ -125,7 +125,7 @@ type RateLimitKey struct { func (x *RateLimitKey) Reset() { *x = RateLimitKey{} - mi := &file_middleware_v1_ratelimit_proto_msgTypes[1] + mi := &file_policies_v1_ratelimit_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -137,7 +137,7 @@ func (x *RateLimitKey) String() string { func (*RateLimitKey) ProtoMessage() {} func (x *RateLimitKey) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_ratelimit_proto_msgTypes[1] + mi := &file_policies_v1_ratelimit_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -150,7 +150,7 @@ func (x *RateLimitKey) ProtoReflect() protoreflect.Message { // Deprecated: Use RateLimitKey.ProtoReflect.Descriptor instead. func (*RateLimitKey) Descriptor() ([]byte, []int) { - return file_middleware_v1_ratelimit_proto_rawDescGZIP(), []int{1} + return file_policies_v1_ratelimit_proto_rawDescGZIP(), []int{1} } func (x *RateLimitKey) GetSource() isRateLimitKey_Source { @@ -273,7 +273,7 @@ type RemoteIpKey struct { func (x *RemoteIpKey) Reset() { *x = RemoteIpKey{} - mi := &file_middleware_v1_ratelimit_proto_msgTypes[2] + mi := &file_policies_v1_ratelimit_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -285,7 +285,7 @@ func (x *RemoteIpKey) String() string { func (*RemoteIpKey) ProtoMessage() {} func (x *RemoteIpKey) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_ratelimit_proto_msgTypes[2] + mi := &file_policies_v1_ratelimit_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -298,7 +298,7 @@ func (x *RemoteIpKey) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoteIpKey.ProtoReflect.Descriptor instead. func (*RemoteIpKey) Descriptor() ([]byte, []int) { - return file_middleware_v1_ratelimit_proto_rawDescGZIP(), []int{2} + return file_policies_v1_ratelimit_proto_rawDescGZIP(), []int{2} } // HeaderKey derives the rate limit key from a request header value. @@ -313,7 +313,7 @@ type HeaderKey struct { func (x *HeaderKey) Reset() { *x = HeaderKey{} - mi := &file_middleware_v1_ratelimit_proto_msgTypes[3] + mi := &file_policies_v1_ratelimit_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -325,7 +325,7 @@ func (x *HeaderKey) String() string { func (*HeaderKey) ProtoMessage() {} func (x *HeaderKey) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_ratelimit_proto_msgTypes[3] + mi := &file_policies_v1_ratelimit_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -338,7 +338,7 @@ func (x *HeaderKey) ProtoReflect() protoreflect.Message { // Deprecated: Use HeaderKey.ProtoReflect.Descriptor instead. func (*HeaderKey) Descriptor() ([]byte, []int) { - return file_middleware_v1_ratelimit_proto_rawDescGZIP(), []int{3} + return file_policies_v1_ratelimit_proto_rawDescGZIP(), []int{3} } func (x *HeaderKey) GetName() string { @@ -360,7 +360,7 @@ type AuthenticatedSubjectKey struct { func (x *AuthenticatedSubjectKey) Reset() { *x = AuthenticatedSubjectKey{} - mi := &file_middleware_v1_ratelimit_proto_msgTypes[4] + mi := &file_policies_v1_ratelimit_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -372,7 +372,7 @@ func (x *AuthenticatedSubjectKey) String() string { func (*AuthenticatedSubjectKey) ProtoMessage() {} func (x *AuthenticatedSubjectKey) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_ratelimit_proto_msgTypes[4] + mi := &file_policies_v1_ratelimit_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -385,7 +385,7 @@ func (x *AuthenticatedSubjectKey) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthenticatedSubjectKey.ProtoReflect.Descriptor instead. func (*AuthenticatedSubjectKey) Descriptor() ([]byte, []int) { - return file_middleware_v1_ratelimit_proto_rawDescGZIP(), []int{4} + return file_policies_v1_ratelimit_proto_rawDescGZIP(), []int{4} } // PathKey derives the rate limit key from the request URL path. @@ -397,7 +397,7 @@ type PathKey struct { func (x *PathKey) Reset() { *x = PathKey{} - mi := &file_middleware_v1_ratelimit_proto_msgTypes[5] + mi := &file_policies_v1_ratelimit_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -409,7 +409,7 @@ func (x *PathKey) String() string { func (*PathKey) ProtoMessage() {} func (x *PathKey) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_ratelimit_proto_msgTypes[5] + mi := &file_policies_v1_ratelimit_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -422,7 +422,7 @@ func (x *PathKey) ProtoReflect() protoreflect.Message { // Deprecated: Use PathKey.ProtoReflect.Descriptor instead. func (*PathKey) Descriptor() ([]byte, []int) { - return file_middleware_v1_ratelimit_proto_rawDescGZIP(), []int{5} + return file_policies_v1_ratelimit_proto_rawDescGZIP(), []int{5} } // PrincipalClaimKey derives the rate limit key from a named claim in the @@ -439,7 +439,7 @@ type PrincipalClaimKey struct { func (x *PrincipalClaimKey) Reset() { *x = PrincipalClaimKey{} - mi := &file_middleware_v1_ratelimit_proto_msgTypes[6] + mi := &file_policies_v1_ratelimit_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -451,7 +451,7 @@ func (x *PrincipalClaimKey) String() string { func (*PrincipalClaimKey) ProtoMessage() {} func (x *PrincipalClaimKey) ProtoReflect() protoreflect.Message { - mi := &file_middleware_v1_ratelimit_proto_msgTypes[6] + mi := &file_policies_v1_ratelimit_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -464,7 +464,7 @@ func (x *PrincipalClaimKey) ProtoReflect() protoreflect.Message { // Deprecated: Use PrincipalClaimKey.ProtoReflect.Descriptor instead. func (*PrincipalClaimKey) Descriptor() ([]byte, []int) { - return file_middleware_v1_ratelimit_proto_rawDescGZIP(), []int{6} + return file_policies_v1_ratelimit_proto_rawDescGZIP(), []int{6} } func (x *PrincipalClaimKey) GetClaimName() string { @@ -474,11 +474,11 @@ func (x *PrincipalClaimKey) GetClaimName() string { return "" } -var File_middleware_v1_ratelimit_proto protoreflect.FileDescriptor +var File_policies_v1_ratelimit_proto protoreflect.FileDescriptor -const file_middleware_v1_ratelimit_proto_rawDesc = "" + +const file_policies_v1_ratelimit_proto_rawDesc = "" + "\n" + - "\x1dmiddleware/v1/ratelimit.proto\x12\vsentinel.v1\"k\n" + + "\x1bpolicies/v1/ratelimit.proto\x12\vsentinel.v1\"k\n" + "\tRateLimit\x12\x14\n" + "\x05limit\x18\x01 \x01(\x03R\x05limit\x12\x1b\n" + "\twindow_ms\x18\x02 \x01(\x03R\bwindowMs\x12+\n" + @@ -501,19 +501,19 @@ const file_middleware_v1_ratelimit_proto_rawDesc = "" + "\x0fcom.sentinel.v1B\x0eRatelimitProtoP\x01Z9github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1\xa2\x02\x03SXX\xaa\x02\vSentinel.V1\xca\x02\vSentinel\\V1\xe2\x02\x17Sentinel\\V1\\GPBMetadata\xea\x02\fSentinel::V1b\x06proto3" var ( - file_middleware_v1_ratelimit_proto_rawDescOnce sync.Once - file_middleware_v1_ratelimit_proto_rawDescData []byte + file_policies_v1_ratelimit_proto_rawDescOnce sync.Once + file_policies_v1_ratelimit_proto_rawDescData []byte ) -func file_middleware_v1_ratelimit_proto_rawDescGZIP() []byte { - file_middleware_v1_ratelimit_proto_rawDescOnce.Do(func() { - file_middleware_v1_ratelimit_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_middleware_v1_ratelimit_proto_rawDesc), len(file_middleware_v1_ratelimit_proto_rawDesc))) +func file_policies_v1_ratelimit_proto_rawDescGZIP() []byte { + file_policies_v1_ratelimit_proto_rawDescOnce.Do(func() { + file_policies_v1_ratelimit_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_policies_v1_ratelimit_proto_rawDesc), len(file_policies_v1_ratelimit_proto_rawDesc))) }) - return file_middleware_v1_ratelimit_proto_rawDescData + return file_policies_v1_ratelimit_proto_rawDescData } -var file_middleware_v1_ratelimit_proto_msgTypes = make([]protoimpl.MessageInfo, 7) -var file_middleware_v1_ratelimit_proto_goTypes = []any{ +var file_policies_v1_ratelimit_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_policies_v1_ratelimit_proto_goTypes = []any{ (*RateLimit)(nil), // 0: sentinel.v1.RateLimit (*RateLimitKey)(nil), // 1: sentinel.v1.RateLimitKey (*RemoteIpKey)(nil), // 2: sentinel.v1.RemoteIpKey @@ -522,7 +522,7 @@ var file_middleware_v1_ratelimit_proto_goTypes = []any{ (*PathKey)(nil), // 5: sentinel.v1.PathKey (*PrincipalClaimKey)(nil), // 6: sentinel.v1.PrincipalClaimKey } -var file_middleware_v1_ratelimit_proto_depIdxs = []int32{ +var file_policies_v1_ratelimit_proto_depIdxs = []int32{ 1, // 0: sentinel.v1.RateLimit.key:type_name -> sentinel.v1.RateLimitKey 2, // 1: sentinel.v1.RateLimitKey.remote_ip:type_name -> sentinel.v1.RemoteIpKey 3, // 2: sentinel.v1.RateLimitKey.header:type_name -> sentinel.v1.HeaderKey @@ -536,12 +536,12 @@ var file_middleware_v1_ratelimit_proto_depIdxs = []int32{ 0, // [0:6] is the sub-list for field type_name } -func init() { file_middleware_v1_ratelimit_proto_init() } -func file_middleware_v1_ratelimit_proto_init() { - if File_middleware_v1_ratelimit_proto != nil { +func init() { file_policies_v1_ratelimit_proto_init() } +func file_policies_v1_ratelimit_proto_init() { + if File_policies_v1_ratelimit_proto != nil { return } - file_middleware_v1_ratelimit_proto_msgTypes[1].OneofWrappers = []any{ + file_policies_v1_ratelimit_proto_msgTypes[1].OneofWrappers = []any{ (*RateLimitKey_RemoteIp)(nil), (*RateLimitKey_Header)(nil), (*RateLimitKey_AuthenticatedSubject)(nil), @@ -552,17 +552,17 @@ func file_middleware_v1_ratelimit_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_middleware_v1_ratelimit_proto_rawDesc), len(file_middleware_v1_ratelimit_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_policies_v1_ratelimit_proto_rawDesc), len(file_policies_v1_ratelimit_proto_rawDesc)), NumEnums: 0, NumMessages: 7, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_middleware_v1_ratelimit_proto_goTypes, - DependencyIndexes: file_middleware_v1_ratelimit_proto_depIdxs, - MessageInfos: file_middleware_v1_ratelimit_proto_msgTypes, + GoTypes: file_policies_v1_ratelimit_proto_goTypes, + DependencyIndexes: file_policies_v1_ratelimit_proto_depIdxs, + MessageInfos: file_policies_v1_ratelimit_proto_msgTypes, }.Build() - File_middleware_v1_ratelimit_proto = out.File - file_middleware_v1_ratelimit_proto_goTypes = nil - file_middleware_v1_ratelimit_proto_depIdxs = nil + File_policies_v1_ratelimit_proto = out.File + file_policies_v1_ratelimit_proto_goTypes = nil + file_policies_v1_ratelimit_proto_depIdxs = nil } diff --git a/pkg/db/environment_find_with_settings.sql_generated.go b/pkg/db/environment_find_with_settings.sql_generated.go index 34c2a9f85e..6487c9f57c 100644 --- a/pkg/db/environment_find_with_settings.sql_generated.go +++ b/pkg/db/environment_find_with_settings.sql_generated.go @@ -7,25 +7,16 @@ package db import ( "context" - - dbtype "github.com/unkeyed/unkey/pkg/db/types" ) const findEnvironmentWithSettingsByProjectIdAndSlug = `-- name: FindEnvironmentWithSettingsByProjectIdAndSlug :one SELECT e.pk, e.id, e.workspace_id, e.project_id, e.slug, e.description, e.sentinel_config, e.delete_protection, e.created_at, e.updated_at, - bs.dockerfile, - bs.docker_context, - rs.port, - rs.cpu_millicores, - rs.memory_mib, - rs.command, - rs.shutdown_signal, - rs.healthcheck, - rs.region_config + ebs.pk, ebs.workspace_id, ebs.environment_id, ebs.dockerfile, ebs.docker_context, ebs.created_at, ebs.updated_at, + ers.pk, ers.workspace_id, ers.environment_id, ers.port, ers.cpu_millicores, ers.memory_mib, ers.command, ers.healthcheck, ers.region_config, ers.shutdown_signal, ers.sentinel_config, ers.created_at, ers.updated_at FROM environments e -INNER JOIN environment_build_settings bs ON bs.environment_id = e.id -INNER JOIN environment_runtime_settings rs ON rs.environment_id = e.id +INNER JOIN environment_build_settings ebs ON ebs.environment_id = e.id +INNER JOIN environment_runtime_settings ers ON ers.environment_id = e.id WHERE e.workspace_id = ? AND e.project_id = ? AND e.slug = ? @@ -38,34 +29,20 @@ type FindEnvironmentWithSettingsByProjectIdAndSlugParams struct { } type FindEnvironmentWithSettingsByProjectIdAndSlugRow struct { - Environment Environment `db:"environment"` - Dockerfile string `db:"dockerfile"` - DockerContext string `db:"docker_context"` - Port int32 `db:"port"` - CpuMillicores int32 `db:"cpu_millicores"` - MemoryMib int32 `db:"memory_mib"` - Command dbtype.StringSlice `db:"command"` - ShutdownSignal EnvironmentRuntimeSettingsShutdownSignal `db:"shutdown_signal"` - Healthcheck dbtype.NullHealthcheck `db:"healthcheck"` - RegionConfig dbtype.RegionConfig `db:"region_config"` + Environment Environment `db:"environment"` + EnvironmentBuildSetting EnvironmentBuildSetting `db:"environment_build_setting"` + EnvironmentRuntimeSetting EnvironmentRuntimeSetting `db:"environment_runtime_setting"` } // FindEnvironmentWithSettingsByProjectIdAndSlug // // SELECT // e.pk, e.id, e.workspace_id, e.project_id, e.slug, e.description, e.sentinel_config, e.delete_protection, e.created_at, e.updated_at, -// bs.dockerfile, -// bs.docker_context, -// rs.port, -// rs.cpu_millicores, -// rs.memory_mib, -// rs.command, -// rs.shutdown_signal, -// rs.healthcheck, -// rs.region_config +// ebs.pk, ebs.workspace_id, ebs.environment_id, ebs.dockerfile, ebs.docker_context, ebs.created_at, ebs.updated_at, +// ers.pk, ers.workspace_id, ers.environment_id, ers.port, ers.cpu_millicores, ers.memory_mib, ers.command, ers.healthcheck, ers.region_config, ers.shutdown_signal, ers.sentinel_config, ers.created_at, ers.updated_at // FROM environments e -// INNER JOIN environment_build_settings bs ON bs.environment_id = e.id -// INNER JOIN environment_runtime_settings rs ON rs.environment_id = e.id +// INNER JOIN environment_build_settings ebs ON ebs.environment_id = e.id +// INNER JOIN environment_runtime_settings ers ON ers.environment_id = e.id // WHERE e.workspace_id = ? // AND e.project_id = ? // AND e.slug = ? @@ -83,15 +60,26 @@ func (q *Queries) FindEnvironmentWithSettingsByProjectIdAndSlug(ctx context.Cont &i.Environment.DeleteProtection, &i.Environment.CreatedAt, &i.Environment.UpdatedAt, - &i.Dockerfile, - &i.DockerContext, - &i.Port, - &i.CpuMillicores, - &i.MemoryMib, - &i.Command, - &i.ShutdownSignal, - &i.Healthcheck, - &i.RegionConfig, + &i.EnvironmentBuildSetting.Pk, + &i.EnvironmentBuildSetting.WorkspaceID, + &i.EnvironmentBuildSetting.EnvironmentID, + &i.EnvironmentBuildSetting.Dockerfile, + &i.EnvironmentBuildSetting.DockerContext, + &i.EnvironmentBuildSetting.CreatedAt, + &i.EnvironmentBuildSetting.UpdatedAt, + &i.EnvironmentRuntimeSetting.Pk, + &i.EnvironmentRuntimeSetting.WorkspaceID, + &i.EnvironmentRuntimeSetting.EnvironmentID, + &i.EnvironmentRuntimeSetting.Port, + &i.EnvironmentRuntimeSetting.CpuMillicores, + &i.EnvironmentRuntimeSetting.MemoryMib, + &i.EnvironmentRuntimeSetting.Command, + &i.EnvironmentRuntimeSetting.Healthcheck, + &i.EnvironmentRuntimeSetting.RegionConfig, + &i.EnvironmentRuntimeSetting.ShutdownSignal, + &i.EnvironmentRuntimeSetting.SentinelConfig, + &i.EnvironmentRuntimeSetting.CreatedAt, + &i.EnvironmentRuntimeSetting.UpdatedAt, ) return i, err } diff --git a/pkg/db/environment_runtime_settings_find_by_environment_id.sql_generated.go b/pkg/db/environment_runtime_settings_find_by_environment_id.sql_generated.go index bd9b022851..a626c697c5 100644 --- a/pkg/db/environment_runtime_settings_find_by_environment_id.sql_generated.go +++ b/pkg/db/environment_runtime_settings_find_by_environment_id.sql_generated.go @@ -10,14 +10,14 @@ import ( ) const findEnvironmentRuntimeSettingsByEnvironmentId = `-- name: FindEnvironmentRuntimeSettingsByEnvironmentId :one -SELECT pk, workspace_id, environment_id, port, cpu_millicores, memory_mib, command, healthcheck, region_config, shutdown_signal, created_at, updated_at +SELECT pk, workspace_id, environment_id, port, cpu_millicores, memory_mib, command, healthcheck, region_config, shutdown_signal, sentinel_config, created_at, updated_at FROM environment_runtime_settings WHERE environment_id = ? ` // FindEnvironmentRuntimeSettingsByEnvironmentId // -// SELECT pk, workspace_id, environment_id, port, cpu_millicores, memory_mib, command, healthcheck, region_config, shutdown_signal, created_at, updated_at +// SELECT pk, workspace_id, environment_id, port, cpu_millicores, memory_mib, command, healthcheck, region_config, shutdown_signal, sentinel_config, created_at, updated_at // FROM environment_runtime_settings // WHERE environment_id = ? func (q *Queries) FindEnvironmentRuntimeSettingsByEnvironmentId(ctx context.Context, db DBTX, environmentID string) (EnvironmentRuntimeSetting, error) { @@ -34,6 +34,7 @@ func (q *Queries) FindEnvironmentRuntimeSettingsByEnvironmentId(ctx context.Cont &i.Healthcheck, &i.RegionConfig, &i.ShutdownSignal, + &i.SentinelConfig, &i.CreatedAt, &i.UpdatedAt, ) diff --git a/pkg/db/models_generated.go b/pkg/db/models_generated.go index 66a9ef14cc..4909b8369c 100644 --- a/pkg/db/models_generated.go +++ b/pkg/db/models_generated.go @@ -1163,6 +1163,7 @@ type EnvironmentRuntimeSetting struct { Healthcheck dbtype.NullHealthcheck `db:"healthcheck"` RegionConfig dbtype.RegionConfig `db:"region_config"` ShutdownSignal EnvironmentRuntimeSettingsShutdownSignal `db:"shutdown_signal"` + SentinelConfig sql.NullString `db:"sentinel_config"` CreatedAt int64 `db:"created_at"` UpdatedAt sql.NullInt64 `db:"updated_at"` } diff --git a/pkg/db/querier_generated.go b/pkg/db/querier_generated.go index bede392f03..82d4c0b90a 100644 --- a/pkg/db/querier_generated.go +++ b/pkg/db/querier_generated.go @@ -301,7 +301,7 @@ type Querier interface { FindEnvironmentByProjectIdAndSlug(ctx context.Context, db DBTX, arg FindEnvironmentByProjectIdAndSlugParams) (Environment, error) //FindEnvironmentRuntimeSettingsByEnvironmentId // - // SELECT pk, workspace_id, environment_id, port, cpu_millicores, memory_mib, command, healthcheck, region_config, shutdown_signal, created_at, updated_at + // SELECT pk, workspace_id, environment_id, port, cpu_millicores, memory_mib, command, healthcheck, region_config, shutdown_signal, sentinel_config, created_at, updated_at // FROM environment_runtime_settings // WHERE environment_id = ? FindEnvironmentRuntimeSettingsByEnvironmentId(ctx context.Context, db DBTX, environmentID string) (EnvironmentRuntimeSetting, error) @@ -315,18 +315,11 @@ type Querier interface { // // SELECT // e.pk, e.id, e.workspace_id, e.project_id, e.slug, e.description, e.sentinel_config, e.delete_protection, e.created_at, e.updated_at, - // bs.dockerfile, - // bs.docker_context, - // rs.port, - // rs.cpu_millicores, - // rs.memory_mib, - // rs.command, - // rs.shutdown_signal, - // rs.healthcheck, - // rs.region_config + // ebs.pk, ebs.workspace_id, ebs.environment_id, ebs.dockerfile, ebs.docker_context, ebs.created_at, ebs.updated_at, + // ers.pk, ers.workspace_id, ers.environment_id, ers.port, ers.cpu_millicores, ers.memory_mib, ers.command, ers.healthcheck, ers.region_config, ers.shutdown_signal, ers.sentinel_config, ers.created_at, ers.updated_at // FROM environments e - // INNER JOIN environment_build_settings bs ON bs.environment_id = e.id - // INNER JOIN environment_runtime_settings rs ON rs.environment_id = e.id + // INNER JOIN environment_build_settings ebs ON ebs.environment_id = e.id + // INNER JOIN environment_runtime_settings ers ON ers.environment_id = e.id // WHERE e.workspace_id = ? // AND e.project_id = ? // AND e.slug = ? diff --git a/pkg/db/queries/environment_find_with_settings.sql b/pkg/db/queries/environment_find_with_settings.sql index f6fa4f474c..32c667ce3f 100644 --- a/pkg/db/queries/environment_find_with_settings.sql +++ b/pkg/db/queries/environment_find_with_settings.sql @@ -1,18 +1,11 @@ -- name: FindEnvironmentWithSettingsByProjectIdAndSlug :one SELECT sqlc.embed(e), - bs.dockerfile, - bs.docker_context, - rs.port, - rs.cpu_millicores, - rs.memory_mib, - rs.command, - rs.shutdown_signal, - rs.healthcheck, - rs.region_config + sqlc.embed(ebs), + sqlc.embed(ers) FROM environments e -INNER JOIN environment_build_settings bs ON bs.environment_id = e.id -INNER JOIN environment_runtime_settings rs ON rs.environment_id = e.id +INNER JOIN environment_build_settings ebs ON ebs.environment_id = e.id +INNER JOIN environment_runtime_settings ers ON ers.environment_id = e.id WHERE e.workspace_id = sqlc.arg(workspace_id) AND e.project_id = sqlc.arg(project_id) AND e.slug = sqlc.arg(slug); diff --git a/pkg/db/schema.sql b/pkg/db/schema.sql index 3e86e2fab6..9531913cb3 100644 --- a/pkg/db/schema.sql +++ b/pkg/db/schema.sql @@ -403,6 +403,7 @@ CREATE TABLE `environment_runtime_settings` ( `healthcheck` json, `region_config` json NOT NULL DEFAULT ('{}'), `shutdown_signal` enum('SIGTERM','SIGINT','SIGQUIT','SIGKILL') NOT NULL DEFAULT 'SIGTERM', + `sentinel_config` longblob, `created_at` bigint NOT NULL, `updated_at` bigint, CONSTRAINT `environment_runtime_settings_pk` PRIMARY KEY(`pk`), diff --git a/svc/ctrl/api/github_webhook.go b/svc/ctrl/api/github_webhook.go index e27858f657..ef90613f32 100644 --- a/svc/ctrl/api/github_webhook.go +++ b/svc/ctrl/api/github_webhook.go @@ -194,9 +194,9 @@ func (s *GitHubWebhook) handlePush(ctx context.Context, w http.ResponseWriter, b WorkspaceID: project.WorkspaceID, ProjectID: project.ID, EnvironmentID: env.ID, - SentinelConfig: env.SentinelConfig, + SentinelConfig: []byte(envSettings.EnvironmentRuntimeSetting.SentinelConfig.String), EncryptedEnvironmentVariables: secretsBlob, - Command: envSettings.Command, + Command: envSettings.EnvironmentRuntimeSetting.Command, Status: db.DeploymentsStatusPending, CreatedAt: now, UpdatedAt: sql.NullInt64{Valid: false}, @@ -207,11 +207,11 @@ func (s *GitHubWebhook) handlePush(ctx context.Context, w http.ResponseWriter, b GitCommitAuthorAvatarUrl: sql.NullString{String: gitCommit.authorAvatarURL, Valid: gitCommit.authorAvatarURL != ""}, GitCommitTimestamp: sql.NullInt64{Int64: gitCommit.timestamp, Valid: gitCommit.timestamp != 0}, OpenapiSpec: sql.NullString{Valid: false}, - CpuMillicores: envSettings.CpuMillicores, - MemoryMib: envSettings.MemoryMib, - Port: envSettings.Port, - ShutdownSignal: db.DeploymentsShutdownSignal(envSettings.ShutdownSignal), - Healthcheck: envSettings.Healthcheck, + CpuMillicores: envSettings.EnvironmentRuntimeSetting.CpuMillicores, + MemoryMib: envSettings.EnvironmentRuntimeSetting.MemoryMib, + Port: envSettings.EnvironmentRuntimeSetting.Port, + ShutdownSignal: db.DeploymentsShutdownSignal(envSettings.EnvironmentRuntimeSetting.ShutdownSignal), + Healthcheck: envSettings.EnvironmentRuntimeSetting.Healthcheck, }) if err != nil { logger.Error("failed to insert deployment", "error", err) @@ -237,8 +237,8 @@ func (s *GitHubWebhook) handlePush(ctx context.Context, w http.ResponseWriter, b InstallationId: repo.InstallationID, Repository: payload.Repository.FullName, CommitSha: payload.After, - ContextPath: envSettings.DockerContext, - DockerfilePath: envSettings.Dockerfile, + ContextPath: envSettings.EnvironmentBuildSetting.DockerContext, + DockerfilePath: envSettings.EnvironmentBuildSetting.Dockerfile, }, }, }) diff --git a/svc/ctrl/services/deployment/create_deployment.go b/svc/ctrl/services/deployment/create_deployment.go index eafe790a77..287cf5b321 100644 --- a/svc/ctrl/services/deployment/create_deployment.go +++ b/svc/ctrl/services/deployment/create_deployment.go @@ -158,9 +158,9 @@ func (s *Service) CreateDeployment( ProjectID: req.Msg.GetProjectId(), EnvironmentID: env.ID, OpenapiSpec: sql.NullString{String: "", Valid: false}, - SentinelConfig: env.SentinelConfig, + SentinelConfig: []byte(envSettings.EnvironmentRuntimeSetting.SentinelConfig.String), EncryptedEnvironmentVariables: secretsBlob, - Command: envSettings.Command, + Command: envSettings.EnvironmentRuntimeSetting.Command, Status: db.DeploymentsStatusPending, CreatedAt: now, UpdatedAt: sql.NullInt64{Valid: false, Int64: 0}, @@ -170,11 +170,11 @@ func (s *Service) CreateDeployment( GitCommitAuthorHandle: sql.NullString{String: gitCommitAuthorHandle, Valid: gitCommitAuthorHandle != ""}, GitCommitAuthorAvatarUrl: sql.NullString{String: gitCommitAuthorAvatarURL, Valid: gitCommitAuthorAvatarURL != ""}, GitCommitTimestamp: sql.NullInt64{Int64: gitCommitTimestamp, Valid: gitCommitTimestamp != 0}, - CpuMillicores: envSettings.CpuMillicores, - MemoryMib: envSettings.MemoryMib, - Port: envSettings.Port, - ShutdownSignal: db.DeploymentsShutdownSignal(envSettings.ShutdownSignal), - Healthcheck: envSettings.Healthcheck, + CpuMillicores: envSettings.EnvironmentRuntimeSetting.CpuMillicores, + MemoryMib: envSettings.EnvironmentRuntimeSetting.MemoryMib, + Port: envSettings.EnvironmentRuntimeSetting.Port, + ShutdownSignal: db.DeploymentsShutdownSignal(envSettings.EnvironmentRuntimeSetting.ShutdownSignal), + Healthcheck: envSettings.EnvironmentRuntimeSetting.Healthcheck, }) if err != nil { logger.Error("failed to insert deployment", "error", err.Error()) diff --git a/svc/krane/internal/sentinel/apply.go b/svc/krane/internal/sentinel/apply.go index 17092de394..bd8b081658 100644 --- a/svc/krane/internal/sentinel/apply.go +++ b/svc/krane/internal/sentinel/apply.go @@ -225,6 +225,14 @@ func (c *Controller) ensureSentinelExists(ctx context.Context, sentinel *ctrlv1. Optional: ptr.P(true), }, }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "redis", + }, + Optional: ptr.P(true), + }, + }, }, Env: []corev1.EnvVar{ diff --git a/svc/sentinel/engine/BUILD.bazel b/svc/sentinel/engine/BUILD.bazel index 07dc9ed321..7d10b9a245 100644 --- a/svc/sentinel/engine/BUILD.bazel +++ b/svc/sentinel/engine/BUILD.bazel @@ -17,7 +17,6 @@ go_library( "//pkg/codes", "//pkg/fault", "//pkg/hash", - "//pkg/logger", "//pkg/rbac", "//pkg/zen", "@org_golang_google_protobuf//encoding/protojson", diff --git a/svc/sentinel/engine/engine.go b/svc/sentinel/engine/engine.go index e812d1c22c..cf4ae1cd09 100644 --- a/svc/sentinel/engine/engine.go +++ b/svc/sentinel/engine/engine.go @@ -7,7 +7,8 @@ import ( sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" "github.com/unkeyed/unkey/internal/services/keys" "github.com/unkeyed/unkey/pkg/clock" - "github.com/unkeyed/unkey/pkg/logger" + "github.com/unkeyed/unkey/pkg/codes" + "github.com/unkeyed/unkey/pkg/fault" "github.com/unkeyed/unkey/pkg/zen" "google.golang.org/protobuf/encoding/protojson" ) @@ -24,7 +25,7 @@ type Config struct { // Evaluator evaluates sentinel middleware policies against incoming requests. type Evaluator interface { - Evaluate(ctx context.Context, sess *zen.Session, req *http.Request, mw *sentinelv1.Middleware) (Result, error) + Evaluate(ctx context.Context, sess *zen.Session, req *http.Request, mw []*sentinelv1.Policy) (Result, error) } // Engine implements Evaluator. @@ -54,25 +55,25 @@ func New(cfg Config) *Engine { // ParseMiddleware performs lenient deserialization of sentinel_config bytes into // a Middleware proto. Returns nil for empty, legacy empty-object, or malformed data // to allow plain pass-through proxying. -func ParseMiddleware(raw []byte) *sentinelv1.Middleware { +func ParseMiddleware(raw []byte) ([]*sentinelv1.Policy, error) { if len(raw) == 0 || string(raw) == "{}" { - return nil + return nil, nil } - mw := &sentinelv1.Middleware{} - if err := protojson.Unmarshal(raw, mw); err != nil { - logger.Warn("failed to unmarshal sentinel middleware config, treating as pass-through", - "error", err.Error(), + cfg := &sentinelv1.Config{} + if err := protojson.Unmarshal(raw, cfg); err != nil { + return nil, fault.Wrap(err, + fault.Code(codes.Sentinel.Internal.InvalidConfiguration.URN()), + fault.Internal("unable to unmarshal sentinel policies"), + fault.Public("The policy datastructure is invalid"), ) - - return nil } - if len(mw.GetPolicies()) == 0 { - return nil + if len(cfg.GetPolicies()) == 0 { + return nil, nil } - return mw + return cfg.GetPolicies(), nil } // Evaluate processes all middleware policies against the incoming request. @@ -82,11 +83,11 @@ func (e *Engine) Evaluate( ctx context.Context, sess *zen.Session, req *http.Request, - mw *sentinelv1.Middleware, + policies []*sentinelv1.Policy, ) (Result, error) { var result Result - for _, policy := range mw.GetPolicies() { + for _, policy := range policies { if !policy.GetEnabled() { continue } diff --git a/svc/sentinel/engine/engine_test.go b/svc/sentinel/engine/engine_test.go index c9ac830300..4689ab5023 100644 --- a/svc/sentinel/engine/engine_test.go +++ b/svc/sentinel/engine/engine_test.go @@ -3,7 +3,6 @@ package engine import ( "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" "google.golang.org/protobuf/encoding/protojson" @@ -11,46 +10,58 @@ import ( func TestParseMiddleware_Nil(t *testing.T) { t.Parallel() - assert.Nil(t, ParseMiddleware(nil)) + policies, err := ParseMiddleware(nil) + require.Nil(t, err) + require.Nil(t, policies) + } func TestParseMiddleware_Empty(t *testing.T) { t.Parallel() - assert.Nil(t, ParseMiddleware([]byte{})) + policies, err := ParseMiddleware([]byte{}) + require.Nil(t, err) + require.Nil(t, policies) } func TestParseMiddleware_EmptyJSON(t *testing.T) { t.Parallel() - assert.Nil(t, ParseMiddleware([]byte("{}"))) + policies, err := ParseMiddleware([]byte("{}")) + require.Nil(t, err) + require.Nil(t, policies) } func TestParseMiddleware_InvalidProto(t *testing.T) { t.Parallel() - assert.Nil(t, ParseMiddleware([]byte("not a valid protobuf"))) + policies, err := ParseMiddleware([]byte("not a valid protobuf")) + require.Error(t, err) + require.Nil(t, policies) } func TestParseMiddleware_NoPolicies(t *testing.T) { t.Parallel() //nolint:exhaustruct - mw := &sentinelv1.Middleware{ + mw := &sentinelv1.Config{ Policies: nil, } raw, err := protojson.Marshal(mw) require.NoError(t, err) - assert.Nil(t, ParseMiddleware(raw)) + + policies, err := ParseMiddleware(raw) + require.Nil(t, err) + require.Nil(t, policies) } func TestParseMiddleware_WithPolicies(t *testing.T) { t.Parallel() //nolint:exhaustruct - mw := &sentinelv1.Middleware{ + mw := &sentinelv1.Config{ Policies: []*sentinelv1.Policy{ { Id: "p1", Name: "key auth", Enabled: true, Config: &sentinelv1.Policy_Keyauth{ - Keyauth: &sentinelv1.KeyAuth{KeySpaceId: "ks_123"}, + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{"ks_123"}}, }, }, }, @@ -58,10 +69,11 @@ func TestParseMiddleware_WithPolicies(t *testing.T) { raw, err := protojson.Marshal(mw) require.NoError(t, err) - result := ParseMiddleware(raw) - require.NotNil(t, result) - assert.Len(t, result.GetPolicies(), 1) - assert.Equal(t, "p1", result.GetPolicies()[0].GetId()) + policies, err := ParseMiddleware(raw) + require.NoError(t, err) + require.NotNil(t, policies) + require.Len(t, policies, 1) + require.Equal(t, "p1", policies[0].GetId()) } func TestSerializePrincipal(t *testing.T) { @@ -83,8 +95,8 @@ func TestSerializePrincipal(t *testing.T) { var roundTripped sentinelv1.Principal err = protojson.Unmarshal([]byte(s), &roundTripped) require.NoError(t, err) - assert.Equal(t, "user_123", roundTripped.GetSubject()) - assert.Equal(t, sentinelv1.PrincipalType_PRINCIPAL_TYPE_API_KEY, roundTripped.GetType()) - assert.Equal(t, "key_abc", roundTripped.GetClaims()["key_id"]) - assert.Equal(t, "ws_456", roundTripped.GetClaims()["workspace_id"]) + require.Equal(t, "user_123", roundTripped.GetSubject()) + require.Equal(t, sentinelv1.PrincipalType_PRINCIPAL_TYPE_API_KEY, roundTripped.GetType()) + require.Equal(t, "key_abc", roundTripped.GetClaims()["key_id"]) + require.Equal(t, "ws_456", roundTripped.GetClaims()["workspace_id"]) } diff --git a/svc/sentinel/engine/integration_test.go b/svc/sentinel/engine/integration_test.go index ebd3663358..69ca34e409 100644 --- a/svc/sentinel/engine/integration_test.go +++ b/svc/sentinel/engine/integration_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" @@ -292,28 +291,25 @@ func TestKeyAuth_ValidKey(t *testing.T) { req.Header.Set("Authorization", "Bearer "+s.RawKey) sess := newSession(t, req) - //nolint:exhaustruct - mw := &sentinelv1.Middleware{ - Policies: []*sentinelv1.Policy{ - { - Id: "auth", - Enabled: true, - Config: &sentinelv1.Policy_Keyauth{ - Keyauth: &sentinelv1.KeyAuth{KeySpaceId: s.KeySpaceID}, - }, + policies := []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{s.KeySpaceID}}, }, }, } - result, err := h.engine.Evaluate(ctx, sess, req, mw) + result, err := h.engine.Evaluate(ctx, sess, req, policies) require.NoError(t, err) require.NotNil(t, result.Principal) // Subject falls back to key ID when no external ID is set - assert.Equal(t, s.KeyID, result.Principal.Subject) - assert.Equal(t, sentinelv1.PrincipalType_PRINCIPAL_TYPE_API_KEY, result.Principal.Type) - assert.Equal(t, s.KeyID, result.Principal.Claims["key_id"]) - assert.Equal(t, s.WorkspaceID, result.Principal.Claims["workspace_id"]) + require.Equal(t, s.KeyID, result.Principal.Subject) + require.Equal(t, sentinelv1.PrincipalType_PRINCIPAL_TYPE_API_KEY, result.Principal.Type) + require.Equal(t, s.KeyID, result.Principal.Claims["key_id"]) + require.Equal(t, s.WorkspaceID, result.Principal.Claims["workspace_id"]) } func TestKeyAuth_ValidKey_WithIdentity(t *testing.T) { @@ -326,27 +322,24 @@ func TestKeyAuth_ValidKey_WithIdentity(t *testing.T) { req.Header.Set("Authorization", "Bearer "+s.RawKey) sess := newSession(t, req) - //nolint:exhaustruct - mw := &sentinelv1.Middleware{ - Policies: []*sentinelv1.Policy{ - { - Id: "auth", - Enabled: true, - Config: &sentinelv1.Policy_Keyauth{ - Keyauth: &sentinelv1.KeyAuth{KeySpaceId: s.KeySpaceID}, - }, + policies := []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{s.KeySpaceID}}, }, }, } - result, err := h.engine.Evaluate(ctx, sess, req, mw) + result, err := h.engine.Evaluate(ctx, sess, req, policies) require.NoError(t, err) require.NotNil(t, result.Principal) // Subject should be the external ID from the identity - assert.NotEqual(t, s.KeyID, result.Principal.Subject) - assert.NotEmpty(t, result.Principal.Claims["identity_id"]) - assert.NotEmpty(t, result.Principal.Claims["external_id"]) + require.NotEqual(t, s.KeyID, result.Principal.Subject) + require.NotEmpty(t, result.Principal.Claims["identity_id"]) + require.NotEmpty(t, result.Principal.Claims["external_id"]) } func TestKeyAuth_MissingKey_Reject(t *testing.T) { @@ -358,135 +351,165 @@ func TestKeyAuth_MissingKey_Reject(t *testing.T) { // No Authorization header sess := newSession(t, req) - //nolint:exhaustruct - mw := &sentinelv1.Middleware{ - Policies: []*sentinelv1.Policy{ - { - Id: "auth", - Enabled: true, - Config: &sentinelv1.Policy_Keyauth{ - Keyauth: &sentinelv1.KeyAuth{ - KeySpaceId: s.KeySpaceID, - AllowAnonymous: false, - }, + policies := []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{ + KeySpaceIds: []string{s.KeySpaceID}, }, }, }, } - _, err := h.engine.Evaluate(ctx, sess, req, mw) + _, err := h.engine.Evaluate(ctx, sess, req, policies) require.Error(t, err) - assert.Contains(t, err.Error(), "missing API key") + require.Contains(t, err.Error(), "missing API key") } -func TestKeyAuth_MissingKey_AllowAnonymous(t *testing.T) { +func TestKeyAuth_InvalidKey_NotFound(t *testing.T) { h := newTestHarness(t) ctx := context.Background() s := h.seed(ctx) req := httptest.NewRequest(http.MethodGet, "/", nil) - // No Authorization header + req.Header.Set("Authorization", "Bearer sk_this_key_does_not_exist") sess := newSession(t, req) //nolint:exhaustruct - mw := &sentinelv1.Middleware{ - Policies: []*sentinelv1.Policy{ - { - Id: "auth", - Enabled: true, - Config: &sentinelv1.Policy_Keyauth{ - Keyauth: &sentinelv1.KeyAuth{ - KeySpaceId: s.KeySpaceID, - AllowAnonymous: true, - }, - }, + policies := []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{s.KeySpaceID}}, }, }, } - result, err := h.engine.Evaluate(ctx, sess, req, mw) - require.NoError(t, err) - assert.Nil(t, result.Principal) + _, err := h.engine.Evaluate(ctx, sess, req, policies) + require.Error(t, err) } -func TestKeyAuth_InvalidKey_NotFound(t *testing.T) { +func TestKeyAuth_InvalidKey_Disabled(t *testing.T) { h := newTestHarness(t) ctx := context.Background() - s := h.seed(ctx) + base := h.seed(ctx) + disabled := h.seedDisabledKey(ctx, base.WorkspaceID, base.KeySpaceID) req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("Authorization", "Bearer sk_this_key_does_not_exist") + req.Header.Set("Authorization", "Bearer "+disabled.RawKey) sess := newSession(t, req) - //nolint:exhaustruct - mw := &sentinelv1.Middleware{ - Policies: []*sentinelv1.Policy{ - { - Id: "auth", - Enabled: true, - Config: &sentinelv1.Policy_Keyauth{ - Keyauth: &sentinelv1.KeyAuth{KeySpaceId: s.KeySpaceID}, - }, + policies := []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{base.KeySpaceID}}, }, }, } - _, err := h.engine.Evaluate(ctx, sess, req, mw) + _, err := h.engine.Evaluate(ctx, sess, req, policies) require.Error(t, err) } -func TestKeyAuth_InvalidKey_Disabled(t *testing.T) { +func TestKeyAuth_WrongKeySpace(t *testing.T) { h := newTestHarness(t) ctx := context.Background() - base := h.seed(ctx) - disabled := h.seedDisabledKey(ctx, base.WorkspaceID, base.KeySpaceID) + s := h.seed(ctx) req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("Authorization", "Bearer "+disabled.RawKey) + req.Header.Set("Authorization", "Bearer "+s.RawKey) sess := newSession(t, req) - //nolint:exhaustruct - mw := &sentinelv1.Middleware{ - Policies: []*sentinelv1.Policy{ - { - Id: "auth", - Enabled: true, - Config: &sentinelv1.Policy_Keyauth{ - Keyauth: &sentinelv1.KeyAuth{KeySpaceId: base.KeySpaceID}, - }, + policies := []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{"ks_wrong_space"}}, }, }, } - _, err := h.engine.Evaluate(ctx, sess, req, mw) + _, err := h.engine.Evaluate(ctx, sess, req, policies) require.Error(t, err) + require.Contains(t, err.Error(), "key does not belong to expected key space") } -func TestKeyAuth_WrongKeySpace(t *testing.T) { +func TestKeyAuth_MultipleKeySpaceIds(t *testing.T) { h := newTestHarness(t) ctx := context.Background() - s := h.seed(ctx) - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("Authorization", "Bearer "+s.RawKey) - sess := newSession(t, req) + s1 := h.seed(ctx) + s2 := h.seed(ctx) - //nolint:exhaustruct - mw := &sentinelv1.Middleware{ - Policies: []*sentinelv1.Policy{ + t.Run("key from first keyspace accepted", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+s1.RawKey) + sess := newSession(t, req) + + policies := []*sentinelv1.Policy{ { Id: "auth", Enabled: true, Config: &sentinelv1.Policy_Keyauth{ - Keyauth: &sentinelv1.KeyAuth{KeySpaceId: "ks_wrong_space"}, + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{s1.KeySpaceID, s2.KeySpaceID}}, }, }, - }, - } + } - _, err := h.engine.Evaluate(ctx, sess, req, mw) - require.Error(t, err) - assert.Contains(t, err.Error(), "key does not belong to expected key space") + result, err := h.engine.Evaluate(ctx, sess, req, policies) + require.NoError(t, err) + require.NotNil(t, result.Principal) + require.Equal(t, s1.KeyID, result.Principal.Subject) + }) + + t.Run("key from second keyspace accepted", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+s2.RawKey) + sess := newSession(t, req) + + policies := []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{s1.KeySpaceID, s2.KeySpaceID}}, + }, + }, + } + + result, err := h.engine.Evaluate(ctx, sess, req, policies) + require.NoError(t, err) + require.NotNil(t, result.Principal) + require.Equal(t, s2.KeyID, result.Principal.Subject) + }) + + t.Run("key not in any listed keyspace rejected", func(t *testing.T) { + s3 := h.seed(ctx) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "Bearer "+s3.RawKey) + sess := newSession(t, req) + + policies := []*sentinelv1.Policy{ + { + Id: "auth", + Enabled: true, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{s1.KeySpaceID, s2.KeySpaceID}}, + }, + }, + } + + _, err := h.engine.Evaluate(ctx, sess, req, policies) + require.Error(t, err) + require.Contains(t, err.Error(), "key does not belong to expected key space") + }) } // --- Engine Evaluate integration tests --- @@ -500,22 +523,19 @@ func TestEvaluate_DisabledPoliciesSkipped(t *testing.T) { req.Header.Set("Authorization", "Bearer "+s.RawKey) sess := newSession(t, req) - //nolint:exhaustruct - mw := &sentinelv1.Middleware{ - Policies: []*sentinelv1.Policy{ - { - Id: "disabled", - Enabled: false, - Config: &sentinelv1.Policy_Keyauth{ - Keyauth: &sentinelv1.KeyAuth{KeySpaceId: s.KeySpaceID}, - }, + policies := []*sentinelv1.Policy{ + { + Id: "disabled", + Enabled: false, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{s.KeySpaceID}}, }, }, } - result, err := h.engine.Evaluate(ctx, sess, req, mw) + result, err := h.engine.Evaluate(ctx, sess, req, policies) require.NoError(t, err) - assert.Nil(t, result.Principal) + require.Nil(t, result.Principal) } func TestEvaluate_MatchFiltering(t *testing.T) { @@ -528,25 +548,22 @@ func TestEvaluate_MatchFiltering(t *testing.T) { req.Header.Set("Authorization", "Bearer "+s.RawKey) sess := newSession(t, req) - //nolint:exhaustruct - mw := &sentinelv1.Middleware{ - Policies: []*sentinelv1.Policy{ - { - Id: "api-auth", - Enabled: true, - Match: []*sentinelv1.MatchExpr{ - {Expr: &sentinelv1.MatchExpr_Path{Path: &sentinelv1.PathMatch{ - Path: &sentinelv1.StringMatch{Match: &sentinelv1.StringMatch_Prefix{Prefix: "/api"}}, - }}}, - }, - Config: &sentinelv1.Policy_Keyauth{ - Keyauth: &sentinelv1.KeyAuth{KeySpaceId: s.KeySpaceID}, - }, + policies := []*sentinelv1.Policy{ + { + Id: "api-auth", + Enabled: true, + Match: []*sentinelv1.MatchExpr{ + {Expr: &sentinelv1.MatchExpr_Path{Path: &sentinelv1.PathMatch{ + Path: &sentinelv1.StringMatch{Match: &sentinelv1.StringMatch_Prefix{Prefix: "/api"}}, + }}}, + }, + Config: &sentinelv1.Policy_Keyauth{ + Keyauth: &sentinelv1.KeyAuth{KeySpaceIds: []string{s.KeySpaceID}}, }, }, } - result, err := h.engine.Evaluate(ctx, sess, req, mw) + result, err := h.engine.Evaluate(ctx, sess, req, policies) require.NoError(t, err) - assert.Nil(t, result.Principal) + require.Nil(t, result.Principal) } diff --git a/svc/sentinel/engine/keyauth.go b/svc/sentinel/engine/keyauth.go index f6caf74720..b8bd052e8f 100644 --- a/svc/sentinel/engine/keyauth.go +++ b/svc/sentinel/engine/keyauth.go @@ -2,9 +2,11 @@ package engine import ( "context" + "fmt" "math" "net/http" "strconv" + "strings" "time" sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" @@ -33,9 +35,6 @@ func (e *KeyAuthExecutor) Execute( ) (*sentinelv1.Principal, error) { rawKey := extractKey(req, cfg.GetLocations()) if rawKey == "" { - if cfg.GetAllowAnonymous() { - return nil, nil - } return nil, fault.New("missing API key", fault.Code(codes.Sentinel.Auth.MissingCredentials.URN()), @@ -64,11 +63,19 @@ func (e *KeyAuthExecutor) Execute( ) } + allowedKeyspace := false + for _, allowedKeyspaceID := range cfg.GetKeySpaceIds() { + if verifier.Key.KeyAuthID == allowedKeyspaceID { + allowedKeyspace = true + break + } + } + // Verify the key belongs to the expected key space - if cfg.GetKeySpaceId() != "" && verifier.Key.KeyAuthID != cfg.GetKeySpaceId() { + if !allowedKeyspace { return nil, fault.New("key does not belong to expected key space", fault.Code(codes.Sentinel.Auth.InvalidKey.URN()), - fault.Internal("key belongs to key space "+verifier.Key.KeyAuthID+", expected "+cfg.GetKeySpaceId()), + fault.Internal(fmt.Sprintf("key belongs to key space %s, expected one of %s", verifier.Key.KeyAuthID, strings.Join(cfg.GetKeySpaceIds(), ","))), fault.Public("Authentication failed. The provided API key is invalid."), ) } diff --git a/svc/sentinel/engine/match.go b/svc/sentinel/engine/match.go index a1111f050b..5ff401de61 100644 --- a/svc/sentinel/engine/match.go +++ b/svc/sentinel/engine/match.go @@ -17,8 +17,8 @@ type regexCache struct { } func newRegexCache() *regexCache { - //nolint:exhaustruct return ®exCache{ + mu: sync.RWMutex{}, cache: make(map[string]*regexp.Regexp), } } diff --git a/svc/sentinel/engine/match_test.go b/svc/sentinel/engine/match_test.go index 62ca6ea105..cb5b5301cf 100644 --- a/svc/sentinel/engine/match_test.go +++ b/svc/sentinel/engine/match_test.go @@ -5,7 +5,6 @@ import ( "net/url" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" sentinelv1 "github.com/unkeyed/unkey/gen/proto/sentinel/v1" ) @@ -15,7 +14,7 @@ func TestMatchesRequest_EmptyList(t *testing.T) { req := &http.Request{Method: "GET", URL: &url.URL{Path: "/api"}, Header: http.Header{}} matched, err := matchesRequest(req, nil, newRegexCache()) require.NoError(t, err) - assert.True(t, matched) + require.True(t, matched) } func TestMatchesRequest_PathExact(t *testing.T) { @@ -32,7 +31,7 @@ func TestMatchesRequest_PathExact(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.True(t, matched) + require.True(t, matched) } func TestMatchesRequest_PathExactMismatch(t *testing.T) { @@ -49,7 +48,7 @@ func TestMatchesRequest_PathExactMismatch(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.False(t, matched) + require.False(t, matched) } func TestMatchesRequest_PathPrefix(t *testing.T) { @@ -66,7 +65,7 @@ func TestMatchesRequest_PathPrefix(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.True(t, matched) + require.True(t, matched) } func TestMatchesRequest_PathRegex(t *testing.T) { @@ -83,7 +82,7 @@ func TestMatchesRequest_PathRegex(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.True(t, matched) + require.True(t, matched) } func TestMatchesRequest_PathCaseInsensitive(t *testing.T) { @@ -103,7 +102,7 @@ func TestMatchesRequest_PathCaseInsensitive(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.True(t, matched) + require.True(t, matched) } func TestMatchesRequest_MethodMatch(t *testing.T) { @@ -135,7 +134,7 @@ func TestMatchesRequest_MethodMatch(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.Equal(t, tt.expected, matched) + require.Equal(t, tt.expected, matched) }) } } @@ -157,7 +156,7 @@ func TestMatchesRequest_HeaderPresent(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.True(t, matched) + require.True(t, matched) } func TestMatchesRequest_HeaderNotPresent(t *testing.T) { @@ -176,7 +175,7 @@ func TestMatchesRequest_HeaderNotPresent(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.False(t, matched) + require.False(t, matched) } func TestMatchesRequest_HeaderValue(t *testing.T) { @@ -198,7 +197,7 @@ func TestMatchesRequest_HeaderValue(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.True(t, matched) + require.True(t, matched) } func TestMatchesRequest_QueryParamPresent(t *testing.T) { @@ -221,7 +220,7 @@ func TestMatchesRequest_QueryParamPresent(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.True(t, matched) + require.True(t, matched) } func TestMatchesRequest_QueryParamValue(t *testing.T) { @@ -246,7 +245,7 @@ func TestMatchesRequest_QueryParamValue(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.True(t, matched) + require.True(t, matched) } func TestMatchesRequest_ANDSemantics(t *testing.T) { @@ -266,7 +265,7 @@ func TestMatchesRequest_ANDSemantics(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.False(t, matched) + require.False(t, matched) } func TestMatchesRequest_ANDSemanticsAllMatch(t *testing.T) { @@ -285,7 +284,7 @@ func TestMatchesRequest_ANDSemanticsAllMatch(t *testing.T) { matched, err := matchesRequest(req, exprs, rc) require.NoError(t, err) - assert.True(t, matched) + require.True(t, matched) } func TestRegexCache(t *testing.T) { @@ -294,12 +293,12 @@ func TestRegexCache(t *testing.T) { re1, err := rc.get(`^/api/v\d+$`) require.NoError(t, err) - assert.True(t, re1.MatchString("/api/v1")) + require.True(t, re1.MatchString("/api/v1")) // Second call should return cached regex re2, err := rc.get(`^/api/v\d+$`) require.NoError(t, err) - assert.Equal(t, re1, re2) + require.Equal(t, re1, re2) } func TestRegexCache_InvalidPattern(t *testing.T) { @@ -307,5 +306,5 @@ func TestRegexCache_InvalidPattern(t *testing.T) { rc := newRegexCache() _, err := rc.get(`[invalid`) - assert.Error(t, err) + require.Error(t, err) } diff --git a/svc/sentinel/proto/config/v1/config.proto b/svc/sentinel/proto/config/v1/config.proto new file mode 100644 index 0000000000..a328669ae8 --- /dev/null +++ b/svc/sentinel/proto/config/v1/config.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package sentinel.v1; + +import "policies/v1/policy.proto"; + +option go_package = "github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1"; + +// Config defines the middleware pipeline for a sentinel deployment. Each +// policy in the list is evaluated in order, forming a chain of request +// processing stages like authentication, rate limiting, and request validation. +message Config { + // Policies are the middleware layers to apply to incoming requests, in + // evaluation order. Each [Policy] combines a match expression (which + // requests it applies to) with a configuration (what it does). Policies + // are evaluated sequentially; if any policy rejects the request, the + // chain short-circuits and returns an error to the client. + repeated Policy policies = 1; +} diff --git a/svc/sentinel/proto/generate.go b/svc/sentinel/proto/generate.go index bececa173a..459561d70b 100644 --- a/svc/sentinel/proto/generate.go +++ b/svc/sentinel/proto/generate.go @@ -1,4 +1,6 @@ package proto -//go:generate go tool buf generate --template ./buf.gen.yaml --path ./middleware -//go:generate go tool buf generate --template ./buf.gen.ts.yaml --path ./middleware +//go:generate go tool buf generate --template ./buf.gen.yaml --path ./policies +//go:generate go tool buf generate --template ./buf.gen.yaml --path ./config +//go:generate go tool buf generate --template ./buf.gen.ts.yaml --path ./policies +//go:generate go tool buf generate --template ./buf.gen.ts.yaml --path ./config diff --git a/svc/sentinel/proto/middleware/v1/basicauth.proto b/svc/sentinel/proto/policies/v1/basicauth.proto similarity index 100% rename from svc/sentinel/proto/middleware/v1/basicauth.proto rename to svc/sentinel/proto/policies/v1/basicauth.proto diff --git a/svc/sentinel/proto/middleware/v1/iprules.proto b/svc/sentinel/proto/policies/v1/iprules.proto similarity index 100% rename from svc/sentinel/proto/middleware/v1/iprules.proto rename to svc/sentinel/proto/policies/v1/iprules.proto diff --git a/svc/sentinel/proto/middleware/v1/jwtauth.proto b/svc/sentinel/proto/policies/v1/jwtauth.proto similarity index 100% rename from svc/sentinel/proto/middleware/v1/jwtauth.proto rename to svc/sentinel/proto/policies/v1/jwtauth.proto diff --git a/svc/sentinel/proto/middleware/v1/keyauth.proto b/svc/sentinel/proto/policies/v1/keyauth.proto similarity index 90% rename from svc/sentinel/proto/middleware/v1/keyauth.proto rename to svc/sentinel/proto/policies/v1/keyauth.proto index 7a5a67dbd9..64c7a362a8 100644 --- a/svc/sentinel/proto/middleware/v1/keyauth.proto +++ b/svc/sentinel/proto/policies/v1/keyauth.proto @@ -26,7 +26,7 @@ message KeyAuth { // The Unkey key space (API) ID to authenticate against. Each key space // contains a set of API keys with shared configuration. This determines // which keys are valid for this policy. - string key_space_id = 1; + repeated string key_space_ids = 1; // Ordered list of locations to extract the API key from. Sentinel tries // each location in order and uses the first one that yields a non-empty @@ -38,14 +38,6 @@ message KeyAuth { // Bearer token, which is the most common convention for API authentication. repeated KeyLocation locations = 2; - // When true, requests that do not contain a key in any of the configured - // locations are allowed through without authentication. No [Principal] is - // produced for anonymous requests. This enables mixed-auth endpoints where - // unauthenticated users get a restricted view and authenticated users get - // full access — the application checks for the presence of identity headers - // to decide. - bool allow_anonymous = 3; - // Optional permission query evaluated against the key's permissions // returned by Unkey's verify API. Uses the same query language as // pkg/rbac.ParseQuery: AND and OR operators with parenthesized grouping, @@ -66,7 +58,7 @@ message KeyAuth { // required permissions. When empty, no permission check is performed. // // Limits: maximum 1000 characters, maximum 100 permission terms. - string permission_query = 5; + optional string permission_query = 5; } // KeyLocation specifies where in the HTTP request to look for an API key. @@ -113,5 +105,3 @@ message QueryParamKeyLocation { // The query parameter name, e.g. "api_key" or "token". string name = 1; } - - diff --git a/svc/sentinel/proto/middleware/v1/match.proto b/svc/sentinel/proto/policies/v1/match.proto similarity index 100% rename from svc/sentinel/proto/middleware/v1/match.proto rename to svc/sentinel/proto/policies/v1/match.proto diff --git a/svc/sentinel/proto/middleware/v1/openapi.proto b/svc/sentinel/proto/policies/v1/openapi.proto similarity index 100% rename from svc/sentinel/proto/middleware/v1/openapi.proto rename to svc/sentinel/proto/policies/v1/openapi.proto diff --git a/svc/sentinel/proto/middleware/v1/middleware.proto b/svc/sentinel/proto/policies/v1/policy.proto similarity index 52% rename from svc/sentinel/proto/middleware/v1/middleware.proto rename to svc/sentinel/proto/policies/v1/policy.proto index a5f365fc85..883a02a011 100644 --- a/svc/sentinel/proto/middleware/v1/middleware.proto +++ b/svc/sentinel/proto/policies/v1/policy.proto @@ -2,49 +2,16 @@ syntax = "proto3"; package sentinel.v1; -import "middleware/v1/basicauth.proto"; -import "middleware/v1/iprules.proto"; -import "middleware/v1/jwtauth.proto"; -import "middleware/v1/keyauth.proto"; -import "middleware/v1/match.proto"; -import "middleware/v1/openapi.proto"; -import "middleware/v1/ratelimit.proto"; +import "policies/v1/basicauth.proto"; +import "policies/v1/iprules.proto"; +import "policies/v1/jwtauth.proto"; +import "policies/v1/keyauth.proto"; +import "policies/v1/match.proto"; +import "policies/v1/openapi.proto"; +import "policies/v1/ratelimit.proto"; option go_package = "github.com/unkeyed/unkey/gen/proto/sentinel/v1;sentinelv1"; -// Middleware is the per-deployment policy configuration for sentinel. -// -// Sentinel is Unkey's reverse proxy. Each deployment gets a Middleware -// configuration that defines which policies apply to incoming requests and in -// what order. When a request arrives, sentinel evaluates every policy's -// match conditions against it, collects the matching policies, and executes -// them sequentially in list order. This gives operators full control over -// request processing without relying on implicit ordering conventions. -// -// A deployment with no policies is a plain pass-through proxy. Adding policies -// incrementally layers on authentication, authorization, traffic shaping, -// and validation — all without touching application code. -message Middleware { - // The ordered list of policies for this deployment. Sentinel executes - // matching policies in exactly this order, so authn policies should appear - // before policies that depend on a [Principal]. - repeated Policy policies = 1; - - // CIDR ranges of trusted proxies sitting in front of sentinel, used to - // derive the real client IP from the X-Forwarded-For header chain. - // Sentinel walks X-Forwarded-For right-to-left, skipping entries that - // fall within a trusted CIDR, and uses the first untrusted entry as the - // client IP. When this list is empty, sentinel uses the direct peer IP - // and ignores X-Forwarded-For entirely — this is the safe default that - // prevents IP spoofing via forged headers. - // - // This setting affects all policies that depend on client IP: [IPRules] - // for allow/deny decisions and [RateLimit] with a [RemoteIpKey] source. - // - // Examples: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - repeated string trusted_proxy_cidrs = 2; -} - // Policy is a single middleware layer in a deployment's configuration. Each policy // combines a match expression (which requests does it apply to?) with a // configuration (what does it do?). This separation is what makes the system diff --git a/svc/sentinel/proto/middleware/v1/principal.proto b/svc/sentinel/proto/policies/v1/principal.proto similarity index 100% rename from svc/sentinel/proto/middleware/v1/principal.proto rename to svc/sentinel/proto/policies/v1/principal.proto diff --git a/svc/sentinel/proto/middleware/v1/ratelimit.proto b/svc/sentinel/proto/policies/v1/ratelimit.proto similarity index 100% rename from svc/sentinel/proto/middleware/v1/ratelimit.proto rename to svc/sentinel/proto/policies/v1/ratelimit.proto diff --git a/svc/sentinel/routes/proxy/handler.go b/svc/sentinel/routes/proxy/handler.go index 276ccafe5b..d4ed74b322 100644 --- a/svc/sentinel/routes/proxy/handler.go +++ b/svc/sentinel/routes/proxy/handler.go @@ -71,7 +71,10 @@ func (h *Handler) Handle(ctx context.Context, sess *zen.Session) error { req.Header.Del(engine.PrincipalHeader) // Evaluate sentinel middleware policies - mw := engine.ParseMiddleware(deployment.SentinelConfig) + mw, err := engine.ParseMiddleware(deployment.SentinelConfig) + if err != nil { + return err + } if mw != nil && h.Engine != nil { result, evalErr := h.Engine.Evaluate(ctx, sess, req, mw) if evalErr != nil { diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/sentinel-settings/keyspaces.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/sentinel-settings/keyspaces.tsx new file mode 100644 index 0000000000..deed0dfcd0 --- /dev/null +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/sentinel-settings/keyspaces.tsx @@ -0,0 +1,223 @@ +"use client"; + +import type { ComboboxOption } from "@/components/ui/combobox"; +import { FormCombobox } from "@/components/ui/form-combobox"; +import { trpc } from "@/lib/trpc/client"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Key, XMark } from "@unkey/icons"; +import { toast } from "@unkey/ui"; +import { useEffect } from "react"; +import { useForm, useWatch } from "react-hook-form"; +import { z } from "zod"; +import { useProjectData } from "../../../data-provider"; +import { FormSettingCard } from "../shared/form-setting-card"; + +const keyspacesSchema = z.object({ + keyspaces: z.array(z.string()).min(1, "Select at least one region"), +}); + +type KeyspacesFormValues = z.infer; + +export const Keyspaces = () => { + const { environments } = useProjectData(); + const environmentId = environments[0]?.id; + + const { data: settingsData } = trpc.deploy.environmentSettings.get.useQuery( + { environmentId: environmentId ?? "" }, + { enabled: Boolean(environmentId) }, + ); + + const { data: availableKeyspaces } = + trpc.deploy.environmentSettings.getAvailableKeyspaces.useQuery(undefined, { + enabled: Boolean(environmentId), + }); + + const defaultKeyspaceIds: string[] = []; + for (const policy of settingsData?.runtimeSettings?.sentinelConfig?.policies ?? []) { + if (policy.config.case === "keyauth") { + defaultKeyspaceIds.push(...policy.config.value.keySpaceIds); + } + } + + return ( + + ); +}; + +type KeyspacesFormProps = { + environmentId: string; + defaultKeyspaceIds: string[]; + availableKeyspaces: Record; +}; + +const KeyspacesForm: React.FC = ({ + environmentId, + defaultKeyspaceIds, + availableKeyspaces, +}) => { + const utils = trpc.useUtils(); + + const { + handleSubmit, + setValue, + formState: { isValid, isSubmitting }, + control, + reset, + } = useForm({ + resolver: zodResolver(keyspacesSchema), + mode: "onChange", + defaultValues: { keyspaces: defaultKeyspaceIds }, + }); + + useEffect(() => { + reset({ keyspaces: defaultKeyspaceIds }); + }, [defaultKeyspaceIds, reset]); + + const currentKeyspaceIds = useWatch({ control, name: "keyspaces" }); + + const unselectedKeyspaceIds = Object.keys(availableKeyspaces).filter( + (r) => !currentKeyspaceIds.includes(r), + ); + + const updateMiddleware = trpc.deploy.environmentSettings.sentinel.updateMiddleware.useMutation({ + onSuccess: () => { + toast.success("Keyspaces updated", { + description: "Deployment keyspaces saved successfully.", + duration: 5000, + }); + utils.deploy.environmentSettings.get.invalidate({ environmentId }); + }, + onError: (err) => { + if (err.data?.code === "BAD_REQUEST") { + toast.error("Invalid keyspaces setting", { + description: err.message || "Please check your input and try again.", + }); + } else { + toast.error("Failed to update keyspaces", { + description: + err.message || + "An unexpected error occurred. Please try again or contact support@unkey.com", + action: { + label: "Contact Support", + onClick: () => window.open("mailto:support@unkey.com", "_blank"), + }, + }); + } + }, + }); + + const onSubmit = async (values: KeyspacesFormValues) => { + await updateMiddleware.mutateAsync({ + environmentId, + keyspaceIds: values.keyspaces, + }); + }; + + const addKeyspace = (region: string) => { + if (region && !currentKeyspaceIds.includes(region)) { + setValue("keyspaces", [...currentKeyspaceIds, region], { + shouldValidate: true, + }); + } + }; + + const removeKeyspace = (region: string) => { + setValue( + "keyspaces", + currentKeyspaceIds.filter((r) => r !== region), + { shouldValidate: true }, + ); + }; + + const hasChanges = + currentKeyspaceIds.length !== defaultKeyspaceIds.length || + currentKeyspaceIds.some((r) => !defaultKeyspaceIds.includes(r)); + + const displayValue = + defaultKeyspaceIds.length === 0 ? ( + "No keyspaces selected" + ) : defaultKeyspaceIds.length <= 2 ? ( + + {defaultKeyspaceIds.map((keyspaceId, i) => ( + + {i > 0 && |} + + {availableKeyspaces[keyspaceId]?.api?.name ?? keyspaceId} + + + ))} + + ) : ( + + {defaultKeyspaceIds.map((keyspaceId) => ( + {availableKeyspaces[keyspaceId]?.api?.name ?? keyspaceId} + ))} + + ); + + const comboboxOptions: ComboboxOption[] = unselectedKeyspaceIds.map((keyspaceId) => ({ + value: keyspaceId, + searchValue: keyspaceId, + label: ( + + {availableKeyspaces[keyspaceId]?.api?.name ?? keyspaceId} + + ), + })); + + return ( + } + title="Keyspaces" + description="Enforce key authentication in your sentinel." + displayValue={displayValue} + onSubmit={handleSubmit(onSubmit)} + canSave={isValid && !isSubmitting && hasChanges} + isSaving={updateMiddleware.isLoading || isSubmitting} + > + Select a keyspace + ) : ( +
+ {currentKeyspaceIds.map((keyspaceId) => ( + + {availableKeyspaces[keyspaceId]?.api?.name ?? keyspaceId} + {currentKeyspaceIds.length > 1 && ( + //biome-ignore lint/a11y/useKeyWithClickEvents: we can't use button here otherwise we'll nest two buttons + { + e.stopPropagation(); + removeKeyspace(keyspaceId); + }} + className="p-0.5 hover:bg-grayA-4 rounded text-grayA-9 hover:text-accent-12 transition-colors" + > + + + )} + + ))} +
+ ) + } + searchPlaceholder="Search keyspaces..." + emptyMessage={
No keyspaces available.
} + /> +
+ ); +}; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/page.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/page.tsx index a037f4d57e..87d201baff 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/page.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { CircleHalfDottedClock, Gear } from "@unkey/icons"; +import { CircleHalfDottedClock, Gear, StackPerspective2 } from "@unkey/icons"; import { SettingCardGroup } from "@unkey/ui"; import { DockerfileSettings } from "./components/build-settings/dockerfile-settings"; @@ -18,6 +18,7 @@ import { Command } from "./components/advanced-settings/command"; import { CustomDomains } from "./components/advanced-settings/custom-domains"; import { EnvVars } from "./components/advanced-settings/env-vars"; +import { Keyspaces } from "./components/sentinel-settings/keyspaces"; import { SettingsGroup } from "./components/shared/settings-group"; export default function SettingsPage() { @@ -61,6 +62,14 @@ export default function SettingsPage() { + } + title="Sentinel configurations" + > + + + + ); diff --git a/web/apps/dashboard/gen/proto/config/v1/config_pb.ts b/web/apps/dashboard/gen/proto/config/v1/config_pb.ts new file mode 100644 index 0000000000..bde3c236bf --- /dev/null +++ b/web/apps/dashboard/gen/proto/config/v1/config_pb.ts @@ -0,0 +1,43 @@ +// @generated by protoc-gen-es v2.8.0 with parameter "target=ts" +// @generated from file config/v1/config.proto (package sentinel.v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Policy } from "../../policies/v1/policy_pb"; +import { file_policies_v1_policy } from "../../policies/v1/policy_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file config/v1/config.proto. + */ +export const file_config_v1_config: GenFile = /*@__PURE__*/ + fileDesc("ChZjb25maWcvdjEvY29uZmlnLnByb3RvEgtzZW50aW5lbC52MSIvCgZDb25maWcSJQoIcG9saWNpZXMYASADKAsyEy5zZW50aW5lbC52MS5Qb2xpY3lCpgEKD2NvbS5zZW50aW5lbC52MUILQ29uZmlnUHJvdG9QAVo5Z2l0aHViLmNvbS91bmtleWVkL3Vua2V5L2dlbi9wcm90by9zZW50aW5lbC92MTtzZW50aW5lbHYxogIDU1hYqgILU2VudGluZWwuVjHKAgtTZW50aW5lbFxWMeICF1NlbnRpbmVsXFYxXEdQQk1ldGFkYXRh6gIMU2VudGluZWw6OlYxYgZwcm90bzM", [file_policies_v1_policy]); + +/** + * Config defines the middleware pipeline for a sentinel deployment. Each + * policy in the list is evaluated in order, forming a chain of request + * processing stages like authentication, rate limiting, and request validation. + * + * @generated from message sentinel.v1.Config + */ +export type Config = Message<"sentinel.v1.Config"> & { + /** + * Policies are the middleware layers to apply to incoming requests, in + * evaluation order. Each [Policy] combines a match expression (which + * requests it applies to) with a configuration (what it does). Policies + * are evaluated sequentially; if any policy rejects the request, the + * chain short-circuits and returns an error to the client. + * + * @generated from field: repeated sentinel.v1.Policy policies = 1; + */ + policies: Policy[]; +}; + +/** + * Describes the message sentinel.v1.Config. + * Use `create(ConfigSchema)` to create a new message. + */ +export const ConfigSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_config_v1_config, 0); + diff --git a/web/apps/dashboard/gen/proto/middleware/v1/middleware_pb.ts b/web/apps/dashboard/gen/proto/middleware/v1/middleware_pb.ts deleted file mode 100644 index 1b8d7597d8..0000000000 --- a/web/apps/dashboard/gen/proto/middleware/v1/middleware_pb.ts +++ /dev/null @@ -1,187 +0,0 @@ -// @generated by protoc-gen-es v2.8.0 with parameter "target=ts" -// @generated from file middleware/v1/middleware.proto (package sentinel.v1, syntax proto3) -/* eslint-disable */ - -import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; -import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; -import type { BasicAuth } from "./basicauth_pb"; -import { file_middleware_v1_basicauth } from "./basicauth_pb"; -import type { IPRules } from "./iprules_pb"; -import { file_middleware_v1_iprules } from "./iprules_pb"; -import type { JWTAuth } from "./jwtauth_pb"; -import { file_middleware_v1_jwtauth } from "./jwtauth_pb"; -import type { KeyAuth } from "./keyauth_pb"; -import { file_middleware_v1_keyauth } from "./keyauth_pb"; -import type { MatchExpr } from "./match_pb"; -import { file_middleware_v1_match } from "./match_pb"; -import type { OpenApiRequestValidation } from "./openapi_pb"; -import { file_middleware_v1_openapi } from "./openapi_pb"; -import type { RateLimit } from "./ratelimit_pb"; -import { file_middleware_v1_ratelimit } from "./ratelimit_pb"; -import type { Message } from "@bufbuild/protobuf"; - -/** - * Describes the file middleware/v1/middleware.proto. - */ -export const file_middleware_v1_middleware: GenFile = /*@__PURE__*/ - fileDesc("Ch5taWRkbGV3YXJlL3YxL21pZGRsZXdhcmUucHJvdG8SC3NlbnRpbmVsLnYxIlAKCk1pZGRsZXdhcmUSJQoIcG9saWNpZXMYASADKAsyEy5zZW50aW5lbC52MS5Qb2xpY3kSGwoTdHJ1c3RlZF9wcm94eV9jaWRycxgCIAMoCSL0AgoGUG9saWN5EgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIlCgVtYXRjaBgEIAMoCzIWLnNlbnRpbmVsLnYxLk1hdGNoRXhwchInCgdrZXlhdXRoGAUgASgLMhQuc2VudGluZWwudjEuS2V5QXV0aEgAEicKB2p3dGF1dGgYBiABKAsyFC5zZW50aW5lbC52MS5KV1RBdXRoSAASKwoJYmFzaWNhdXRoGAcgASgLMhYuc2VudGluZWwudjEuQmFzaWNBdXRoSAASKwoJcmF0ZWxpbWl0GAggASgLMhYuc2VudGluZWwudjEuUmF0ZUxpbWl0SAASKAoIaXBfcnVsZXMYCSABKAsyFC5zZW50aW5lbC52MS5JUFJ1bGVzSAASOAoHb3BlbmFwaRgKIAEoCzIlLnNlbnRpbmVsLnYxLk9wZW5BcGlSZXF1ZXN0VmFsaWRhdGlvbkgAQggKBmNvbmZpZ0KqAQoPY29tLnNlbnRpbmVsLnYxQg9NaWRkbGV3YXJlUHJvdG9QAVo5Z2l0aHViLmNvbS91bmtleWVkL3Vua2V5L2dlbi9wcm90by9zZW50aW5lbC92MTtzZW50aW5lbHYxogIDU1hYqgILU2VudGluZWwuVjHKAgtTZW50aW5lbFxWMeICF1NlbnRpbmVsXFYxXEdQQk1ldGFkYXRh6gIMU2VudGluZWw6OlYxYgZwcm90bzM", [file_middleware_v1_basicauth, file_middleware_v1_iprules, file_middleware_v1_jwtauth, file_middleware_v1_keyauth, file_middleware_v1_match, file_middleware_v1_openapi, file_middleware_v1_ratelimit]); - -/** - * Middleware is the per-deployment policy configuration for sentinel. - * - * Sentinel is Unkey's reverse proxy. Each deployment gets a Middleware - * configuration that defines which policies apply to incoming requests and in - * what order. When a request arrives, sentinel evaluates every policy's - * match conditions against it, collects the matching policies, and executes - * them sequentially in list order. This gives operators full control over - * request processing without relying on implicit ordering conventions. - * - * A deployment with no policies is a plain pass-through proxy. Adding policies - * incrementally layers on authentication, authorization, traffic shaping, - * and validation — all without touching application code. - * - * @generated from message sentinel.v1.Middleware - */ -export type Middleware = Message<"sentinel.v1.Middleware"> & { - /** - * The ordered list of policies for this deployment. Sentinel executes - * matching policies in exactly this order, so authn policies should appear - * before policies that depend on a [Principal]. - * - * @generated from field: repeated sentinel.v1.Policy policies = 1; - */ - policies: Policy[]; - - /** - * CIDR ranges of trusted proxies sitting in front of sentinel, used to - * derive the real client IP from the X-Forwarded-For header chain. - * Sentinel walks X-Forwarded-For right-to-left, skipping entries that - * fall within a trusted CIDR, and uses the first untrusted entry as the - * client IP. When this list is empty, sentinel uses the direct peer IP - * and ignores X-Forwarded-For entirely — this is the safe default that - * prevents IP spoofing via forged headers. - * - * This setting affects all policies that depend on client IP: [IPRules] - * for allow/deny decisions and [RateLimit] with a [RemoteIpKey] source. - * - * Examples: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - * - * @generated from field: repeated string trusted_proxy_cidrs = 2; - */ - trustedProxyCidrs: string[]; -}; - -/** - * Describes the message sentinel.v1.Middleware. - * Use `create(MiddlewareSchema)` to create a new message. - */ -export const MiddlewareSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_middleware, 0); - -/** - * Policy is a single middleware layer in a deployment's configuration. Each policy - * combines a match expression (which requests does it apply to?) with a - * configuration (what does it do?). This separation is what makes the system - * composable: the same rate limiter config can be scoped to POST /api/* - * without the rate limiter needing to know anything about path matching. - * - * Policies carry a stable id for correlation across logs, metrics, and - * debugging. The disabled flag allows operators to disable a policy without - * removing it from config, which is critical for incident response — you can - * turn off a misbehaving policy and re-enable it once the issue is resolved, - * without losing the configuration or triggering a full redeploy. - * - * @generated from message sentinel.v1.Policy - */ -export type Policy = Message<"sentinel.v1.Policy"> & { - /** - * Stable identifier for this policy, used in log entries, metrics labels, - * and error messages. Should be unique within a deployment's Middleware - * config. Typically a UUID or a slug like "api-ratelimit". - * - * @generated from field: string id = 1; - */ - id: string; - - /** - * Human-friendly label displayed in the dashboard and audit logs. - * Does not affect policy behavior. - * - * @generated from field: string name = 2; - */ - name: string; - - /** - * When false, sentinel skips this policy entirely during evaluation. - * This allows operators to toggle policies on and off without modifying - * or removing the underlying configuration, which is useful during - * incidents, gradual rollouts, and debugging. - * - * @generated from field: bool enabled = 3; - */ - enabled: boolean; - - /** - * Match conditions that determine which requests this policy applies to. - * All entries must match for the policy to run (implicit AND). An empty - * list matches all requests — this is the common case for global policies - * like IP allowlists or rate limiting. - * - * For OR semantics, create separate policies with the same config and - * different match lists. - * - * @generated from field: repeated sentinel.v1.MatchExpr match = 4; - */ - match: MatchExpr[]; - - /** - * The policy configuration. Exactly one must be set. - * - * @generated from oneof sentinel.v1.Policy.config - */ - config: { - /** - * @generated from field: sentinel.v1.KeyAuth keyauth = 5; - */ - value: KeyAuth; - case: "keyauth"; - } | { - /** - * @generated from field: sentinel.v1.JWTAuth jwtauth = 6; - */ - value: JWTAuth; - case: "jwtauth"; - } | { - /** - * @generated from field: sentinel.v1.BasicAuth basicauth = 7; - */ - value: BasicAuth; - case: "basicauth"; - } | { - /** - * @generated from field: sentinel.v1.RateLimit ratelimit = 8; - */ - value: RateLimit; - case: "ratelimit"; - } | { - /** - * @generated from field: sentinel.v1.IPRules ip_rules = 9; - */ - value: IPRules; - case: "ipRules"; - } | { - /** - * @generated from field: sentinel.v1.OpenApiRequestValidation openapi = 10; - */ - value: OpenApiRequestValidation; - case: "openapi"; - } | { case: undefined; value?: undefined }; -}; - -/** - * Describes the message sentinel.v1.Policy. - * Use `create(PolicySchema)` to create a new message. - */ -export const PolicySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_middleware, 1); - diff --git a/web/apps/dashboard/gen/proto/middleware/v1/basicauth_pb.ts b/web/apps/dashboard/gen/proto/policies/v1/basicauth_pb.ts similarity index 81% rename from web/apps/dashboard/gen/proto/middleware/v1/basicauth_pb.ts rename to web/apps/dashboard/gen/proto/policies/v1/basicauth_pb.ts index a226e24e19..bee75093ea 100644 --- a/web/apps/dashboard/gen/proto/middleware/v1/basicauth_pb.ts +++ b/web/apps/dashboard/gen/proto/policies/v1/basicauth_pb.ts @@ -1,5 +1,5 @@ // @generated by protoc-gen-es v2.8.0 with parameter "target=ts" -// @generated from file middleware/v1/basicauth.proto (package sentinel.v1, syntax proto3) +// @generated from file policies/v1/basicauth.proto (package sentinel.v1, syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; @@ -7,10 +7,10 @@ import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** - * Describes the file middleware/v1/basicauth.proto. + * Describes the file policies/v1/basicauth.proto. */ -export const file_middleware_v1_basicauth: GenFile = /*@__PURE__*/ - fileDesc("Ch1taWRkbGV3YXJlL3YxL2Jhc2ljYXV0aC5wcm90bxILc2VudGluZWwudjEiQgoJQmFzaWNBdXRoEjUKC2NyZWRlbnRpYWxzGAEgAygLMiAuc2VudGluZWwudjEuQmFzaWNBdXRoQ3JlZGVudGlhbCI+ChNCYXNpY0F1dGhDcmVkZW50aWFsEhAKCHVzZXJuYW1lGAEgASgJEhUKDXBhc3N3b3JkX2hhc2gYAiABKAlCqQEKD2NvbS5zZW50aW5lbC52MUIOQmFzaWNhdXRoUHJvdG9QAVo5Z2l0aHViLmNvbS91bmtleWVkL3Vua2V5L2dlbi9wcm90by9zZW50aW5lbC92MTtzZW50aW5lbHYxogIDU1hYqgILU2VudGluZWwuVjHKAgtTZW50aW5lbFxWMeICF1NlbnRpbmVsXFYxXEdQQk1ldGFkYXRh6gIMU2VudGluZWw6OlYxYgZwcm90bzM"); +export const file_policies_v1_basicauth: GenFile = /*@__PURE__*/ + fileDesc("Chtwb2xpY2llcy92MS9iYXNpY2F1dGgucHJvdG8SC3NlbnRpbmVsLnYxIkIKCUJhc2ljQXV0aBI1CgtjcmVkZW50aWFscxgBIAMoCzIgLnNlbnRpbmVsLnYxLkJhc2ljQXV0aENyZWRlbnRpYWwiPgoTQmFzaWNBdXRoQ3JlZGVudGlhbBIQCgh1c2VybmFtZRgBIAEoCRIVCg1wYXNzd29yZF9oYXNoGAIgASgJQqkBCg9jb20uc2VudGluZWwudjFCDkJhc2ljYXV0aFByb3RvUAFaOWdpdGh1Yi5jb20vdW5rZXllZC91bmtleS9nZW4vcHJvdG8vc2VudGluZWwvdjE7c2VudGluZWx2MaICA1NYWKoCC1NlbnRpbmVsLlYxygILU2VudGluZWxcVjHiAhdTZW50aW5lbFxWMVxHUEJNZXRhZGF0YeoCDFNlbnRpbmVsOjpWMWIGcHJvdG8z"); /** * BasicAuth validates HTTP Basic credentials (RFC 7617) and produces a @@ -54,7 +54,7 @@ export type BasicAuth = Message<"sentinel.v1.BasicAuth"> & { * Use `create(BasicAuthSchema)` to create a new message. */ export const BasicAuthSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_basicauth, 0); + messageDesc(file_policies_v1_basicauth, 0); /** * BasicAuthCredential represents a single valid username and password @@ -89,5 +89,5 @@ export type BasicAuthCredential = Message<"sentinel.v1.BasicAuthCredential"> & { * Use `create(BasicAuthCredentialSchema)` to create a new message. */ export const BasicAuthCredentialSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_basicauth, 1); + messageDesc(file_policies_v1_basicauth, 1); diff --git a/web/apps/dashboard/gen/proto/middleware/v1/iprules_pb.ts b/web/apps/dashboard/gen/proto/policies/v1/iprules_pb.ts similarity index 79% rename from web/apps/dashboard/gen/proto/middleware/v1/iprules_pb.ts rename to web/apps/dashboard/gen/proto/policies/v1/iprules_pb.ts index e3799e482e..d25ed026d4 100644 --- a/web/apps/dashboard/gen/proto/middleware/v1/iprules_pb.ts +++ b/web/apps/dashboard/gen/proto/policies/v1/iprules_pb.ts @@ -1,5 +1,5 @@ // @generated by protoc-gen-es v2.8.0 with parameter "target=ts" -// @generated from file middleware/v1/iprules.proto (package sentinel.v1, syntax proto3) +// @generated from file policies/v1/iprules.proto (package sentinel.v1, syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; @@ -7,10 +7,10 @@ import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** - * Describes the file middleware/v1/iprules.proto. + * Describes the file policies/v1/iprules.proto. */ -export const file_middleware_v1_iprules: GenFile = /*@__PURE__*/ - fileDesc("ChttaWRkbGV3YXJlL3YxL2lwcnVsZXMucHJvdG8SC3NlbnRpbmVsLnYxIiYKB0lQUnVsZXMSDQoFYWxsb3cYASADKAkSDAoEZGVueRgCIAMoCUKnAQoPY29tLnNlbnRpbmVsLnYxQgxJcHJ1bGVzUHJvdG9QAVo5Z2l0aHViLmNvbS91bmtleWVkL3Vua2V5L2dlbi9wcm90by9zZW50aW5lbC92MTtzZW50aW5lbHYxogIDU1hYqgILU2VudGluZWwuVjHKAgtTZW50aW5lbFxWMeICF1NlbnRpbmVsXFYxXEdQQk1ldGFkYXRh6gIMU2VudGluZWw6OlYxYgZwcm90bzM"); +export const file_policies_v1_iprules: GenFile = /*@__PURE__*/ + fileDesc("Chlwb2xpY2llcy92MS9pcHJ1bGVzLnByb3RvEgtzZW50aW5lbC52MSImCgdJUFJ1bGVzEg0KBWFsbG93GAEgAygJEgwKBGRlbnkYAiADKAlCpwEKD2NvbS5zZW50aW5lbC52MUIMSXBydWxlc1Byb3RvUAFaOWdpdGh1Yi5jb20vdW5rZXllZC91bmtleS9nZW4vcHJvdG8vc2VudGluZWwvdjE7c2VudGluZWx2MaICA1NYWKoCC1NlbnRpbmVsLlYxygILU2VudGluZWxcVjHiAhdTZW50aW5lbFxWMVxHUEJNZXRhZGF0YeoCDFNlbnRpbmVsOjpWMWIGcHJvdG8z"); /** * IPRules allows or denies requests based on the client's IP address, @@ -64,5 +64,5 @@ export type IPRules = Message<"sentinel.v1.IPRules"> & { * Use `create(IPRulesSchema)` to create a new message. */ export const IPRulesSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_iprules, 0); + messageDesc(file_policies_v1_iprules, 0); diff --git a/web/apps/dashboard/gen/proto/middleware/v1/jwtauth_pb.ts b/web/apps/dashboard/gen/proto/policies/v1/jwtauth_pb.ts similarity index 87% rename from web/apps/dashboard/gen/proto/middleware/v1/jwtauth_pb.ts rename to web/apps/dashboard/gen/proto/policies/v1/jwtauth_pb.ts index c67cdf0e32..92a9e70308 100644 --- a/web/apps/dashboard/gen/proto/middleware/v1/jwtauth_pb.ts +++ b/web/apps/dashboard/gen/proto/policies/v1/jwtauth_pb.ts @@ -1,5 +1,5 @@ // @generated by protoc-gen-es v2.8.0 with parameter "target=ts" -// @generated from file middleware/v1/jwtauth.proto (package sentinel.v1, syntax proto3) +// @generated from file policies/v1/jwtauth.proto (package sentinel.v1, syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; @@ -7,10 +7,10 @@ import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** - * Describes the file middleware/v1/jwtauth.proto. + * Describes the file policies/v1/jwtauth.proto. */ -export const file_middleware_v1_jwtauth: GenFile = /*@__PURE__*/ - fileDesc("ChttaWRkbGV3YXJlL3YxL2p3dGF1dGgucHJvdG8SC3NlbnRpbmVsLnYxIooCCgdKV1RBdXRoEhIKCGp3a3NfdXJpGAEgASgJSAASFQoLb2lkY19pc3N1ZXIYAiABKAlIABIYCg5wdWJsaWNfa2V5X3BlbRgLIAEoDEgAEg4KBmlzc3VlchgDIAEoCRIRCglhdWRpZW5jZXMYBCADKAkSEgoKYWxnb3JpdGhtcxgFIAMoCRIVCg1zdWJqZWN0X2NsYWltGAYgASgJEhYKDmZvcndhcmRfY2xhaW1zGAcgAygJEhcKD2FsbG93X2Fub255bW91cxgIIAEoCBIVCg1jbG9ja19za2V3X21zGAkgASgDEhUKDWp3a3NfY2FjaGVfbXMYCiABKANCDQoLandrc19zb3VyY2VCpwEKD2NvbS5zZW50aW5lbC52MUIMSnd0YXV0aFByb3RvUAFaOWdpdGh1Yi5jb20vdW5rZXllZC91bmtleS9nZW4vcHJvdG8vc2VudGluZWwvdjE7c2VudGluZWx2MaICA1NYWKoCC1NlbnRpbmVsLlYxygILU2VudGluZWxcVjHiAhdTZW50aW5lbFxWMVxHUEJNZXRhZGF0YeoCDFNlbnRpbmVsOjpWMWIGcHJvdG8z"); +export const file_policies_v1_jwtauth: GenFile = /*@__PURE__*/ + fileDesc("Chlwb2xpY2llcy92MS9qd3RhdXRoLnByb3RvEgtzZW50aW5lbC52MSKKAgoHSldUQXV0aBISCghqd2tzX3VyaRgBIAEoCUgAEhUKC29pZGNfaXNzdWVyGAIgASgJSAASGAoOcHVibGljX2tleV9wZW0YCyABKAxIABIOCgZpc3N1ZXIYAyABKAkSEQoJYXVkaWVuY2VzGAQgAygJEhIKCmFsZ29yaXRobXMYBSADKAkSFQoNc3ViamVjdF9jbGFpbRgGIAEoCRIWCg5mb3J3YXJkX2NsYWltcxgHIAMoCRIXCg9hbGxvd19hbm9ueW1vdXMYCCABKAgSFQoNY2xvY2tfc2tld19tcxgJIAEoAxIVCg1qd2tzX2NhY2hlX21zGAogASgDQg0KC2p3a3Nfc291cmNlQqcBCg9jb20uc2VudGluZWwudjFCDEp3dGF1dGhQcm90b1ABWjlnaXRodWIuY29tL3Vua2V5ZWQvdW5rZXkvZ2VuL3Byb3RvL3NlbnRpbmVsL3YxO3NlbnRpbmVsdjGiAgNTWFiqAgtTZW50aW5lbC5WMcoCC1NlbnRpbmVsXFYx4gIXU2VudGluZWxcVjFcR1BCTWV0YWRhdGHqAgxTZW50aW5lbDo6VjFiBnByb3RvMw"); /** * JWTAuth validates Bearer JSON Web Tokens using JWKS (JSON Web Key Sets) @@ -171,5 +171,5 @@ export type JWTAuth = Message<"sentinel.v1.JWTAuth"> & { * Use `create(JWTAuthSchema)` to create a new message. */ export const JWTAuthSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_jwtauth, 0); + messageDesc(file_policies_v1_jwtauth, 0); diff --git a/web/apps/dashboard/gen/proto/middleware/v1/keyauth_pb.ts b/web/apps/dashboard/gen/proto/policies/v1/keyauth_pb.ts similarity index 79% rename from web/apps/dashboard/gen/proto/middleware/v1/keyauth_pb.ts rename to web/apps/dashboard/gen/proto/policies/v1/keyauth_pb.ts index 336a6cd837..c060713c22 100644 --- a/web/apps/dashboard/gen/proto/middleware/v1/keyauth_pb.ts +++ b/web/apps/dashboard/gen/proto/policies/v1/keyauth_pb.ts @@ -1,5 +1,5 @@ // @generated by protoc-gen-es v2.8.0 with parameter "target=ts" -// @generated from file middleware/v1/keyauth.proto (package sentinel.v1, syntax proto3) +// @generated from file policies/v1/keyauth.proto (package sentinel.v1, syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; @@ -7,10 +7,10 @@ import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** - * Describes the file middleware/v1/keyauth.proto. + * Describes the file policies/v1/keyauth.proto. */ -export const file_middleware_v1_keyauth: GenFile = /*@__PURE__*/ - fileDesc("ChttaWRkbGV3YXJlL3YxL2tleWF1dGgucHJvdG8SC3NlbnRpbmVsLnYxIn8KB0tleUF1dGgSFAoMa2V5X3NwYWNlX2lkGAEgASgJEisKCWxvY2F0aW9ucxgCIAMoCzIYLnNlbnRpbmVsLnYxLktleUxvY2F0aW9uEhcKD2FsbG93X2Fub255bW91cxgDIAEoCBIYChBwZXJtaXNzaW9uX3F1ZXJ5GAUgASgJIroBCgtLZXlMb2NhdGlvbhIyCgZiZWFyZXIYASABKAsyIC5zZW50aW5lbC52MS5CZWFyZXJUb2tlbkxvY2F0aW9uSAASMAoGaGVhZGVyGAIgASgLMh4uc2VudGluZWwudjEuSGVhZGVyS2V5TG9jYXRpb25IABI5CgtxdWVyeV9wYXJhbRgDIAEoCzIiLnNlbnRpbmVsLnYxLlF1ZXJ5UGFyYW1LZXlMb2NhdGlvbkgAQgoKCGxvY2F0aW9uIhUKE0JlYXJlclRva2VuTG9jYXRpb24iNwoRSGVhZGVyS2V5TG9jYXRpb24SDAoEbmFtZRgBIAEoCRIUCgxzdHJpcF9wcmVmaXgYAiABKAkiJQoVUXVlcnlQYXJhbUtleUxvY2F0aW9uEgwKBG5hbWUYASABKAlCpwEKD2NvbS5zZW50aW5lbC52MUIMS2V5YXV0aFByb3RvUAFaOWdpdGh1Yi5jb20vdW5rZXllZC91bmtleS9nZW4vcHJvdG8vc2VudGluZWwvdjE7c2VudGluZWx2MaICA1NYWKoCC1NlbnRpbmVsLlYxygILU2VudGluZWxcVjHiAhdTZW50aW5lbFxWMVxHUEJNZXRhZGF0YeoCDFNlbnRpbmVsOjpWMWIGcHJvdG8z"); +export const file_policies_v1_keyauth: GenFile = /*@__PURE__*/ + fileDesc("Chlwb2xpY2llcy92MS9rZXlhdXRoLnByb3RvEgtzZW50aW5lbC52MSKBAQoHS2V5QXV0aBIVCg1rZXlfc3BhY2VfaWRzGAEgAygJEisKCWxvY2F0aW9ucxgCIAMoCzIYLnNlbnRpbmVsLnYxLktleUxvY2F0aW9uEh0KEHBlcm1pc3Npb25fcXVlcnkYBSABKAlIAIgBAUITChFfcGVybWlzc2lvbl9xdWVyeSK6AQoLS2V5TG9jYXRpb24SMgoGYmVhcmVyGAEgASgLMiAuc2VudGluZWwudjEuQmVhcmVyVG9rZW5Mb2NhdGlvbkgAEjAKBmhlYWRlchgCIAEoCzIeLnNlbnRpbmVsLnYxLkhlYWRlcktleUxvY2F0aW9uSAASOQoLcXVlcnlfcGFyYW0YAyABKAsyIi5zZW50aW5lbC52MS5RdWVyeVBhcmFtS2V5TG9jYXRpb25IAEIKCghsb2NhdGlvbiIVChNCZWFyZXJUb2tlbkxvY2F0aW9uIjcKEUhlYWRlcktleUxvY2F0aW9uEgwKBG5hbWUYASABKAkSFAoMc3RyaXBfcHJlZml4GAIgASgJIiUKFVF1ZXJ5UGFyYW1LZXlMb2NhdGlvbhIMCgRuYW1lGAEgASgJQqcBCg9jb20uc2VudGluZWwudjFCDEtleWF1dGhQcm90b1ABWjlnaXRodWIuY29tL3Vua2V5ZWQvdW5rZXkvZ2VuL3Byb3RvL3NlbnRpbmVsL3YxO3NlbnRpbmVsdjGiAgNTWFiqAgtTZW50aW5lbC5WMcoCC1NlbnRpbmVsXFYx4gIXU2VudGluZWxcVjFcR1BCTWV0YWRhdGHqAgxTZW50aW5lbDo6VjFiBnByb3RvMw"); /** * KeyAuth authenticates requests using Unkey API keys. This is the primary @@ -40,9 +40,9 @@ export type KeyAuth = Message<"sentinel.v1.KeyAuth"> & { * contains a set of API keys with shared configuration. This determines * which keys are valid for this policy. * - * @generated from field: string key_space_id = 1; + * @generated from field: repeated string key_space_ids = 1; */ - keySpaceId: string; + keySpaceIds: string[]; /** * Ordered list of locations to extract the API key from. Sentinel tries @@ -58,18 +58,6 @@ export type KeyAuth = Message<"sentinel.v1.KeyAuth"> & { */ locations: KeyLocation[]; - /** - * When true, requests that do not contain a key in any of the configured - * locations are allowed through without authentication. No [Principal] is - * produced for anonymous requests. This enables mixed-auth endpoints where - * unauthenticated users get a restricted view and authenticated users get - * full access — the application checks for the presence of identity headers - * to decide. - * - * @generated from field: bool allow_anonymous = 3; - */ - allowAnonymous: boolean; - /** * Optional permission query evaluated against the key's permissions * returned by Unkey's verify API. Uses the same query language as @@ -92,9 +80,9 @@ export type KeyAuth = Message<"sentinel.v1.KeyAuth"> & { * * Limits: maximum 1000 characters, maximum 100 permission terms. * - * @generated from field: string permission_query = 5; + * @generated from field: optional string permission_query = 5; */ - permissionQuery: string; + permissionQuery?: string; }; /** @@ -102,7 +90,7 @@ export type KeyAuth = Message<"sentinel.v1.KeyAuth"> & { * Use `create(KeyAuthSchema)` to create a new message. */ export const KeyAuthSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_keyauth, 0); + messageDesc(file_policies_v1_keyauth, 0); /** * KeyLocation specifies where in the HTTP request to look for an API key. @@ -153,7 +141,7 @@ export type KeyLocation = Message<"sentinel.v1.KeyLocation"> & { * Use `create(KeyLocationSchema)` to create a new message. */ export const KeyLocationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_keyauth, 1); + messageDesc(file_policies_v1_keyauth, 1); /** * BearerTokenLocation extracts the API key from the Authorization header @@ -170,7 +158,7 @@ export type BearerTokenLocation = Message<"sentinel.v1.BearerTokenLocation"> & { * Use `create(BearerTokenLocationSchema)` to create a new message. */ export const BearerTokenLocationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_keyauth, 2); + messageDesc(file_policies_v1_keyauth, 2); /** * HeaderKeyLocation extracts the API key from a named request header. This @@ -204,7 +192,7 @@ export type HeaderKeyLocation = Message<"sentinel.v1.HeaderKeyLocation"> & { * Use `create(HeaderKeyLocationSchema)` to create a new message. */ export const HeaderKeyLocationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_keyauth, 3); + messageDesc(file_policies_v1_keyauth, 3); /** * QueryParamKeyLocation extracts the API key from a URL query parameter. @@ -225,5 +213,5 @@ export type QueryParamKeyLocation = Message<"sentinel.v1.QueryParamKeyLocation"> * Use `create(QueryParamKeyLocationSchema)` to create a new message. */ export const QueryParamKeyLocationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_keyauth, 4); + messageDesc(file_policies_v1_keyauth, 4); diff --git a/web/apps/dashboard/gen/proto/middleware/v1/match_pb.ts b/web/apps/dashboard/gen/proto/policies/v1/match_pb.ts similarity index 84% rename from web/apps/dashboard/gen/proto/middleware/v1/match_pb.ts rename to web/apps/dashboard/gen/proto/policies/v1/match_pb.ts index cc480a7b83..212a8551c9 100644 --- a/web/apps/dashboard/gen/proto/middleware/v1/match_pb.ts +++ b/web/apps/dashboard/gen/proto/policies/v1/match_pb.ts @@ -1,5 +1,5 @@ // @generated by protoc-gen-es v2.8.0 with parameter "target=ts" -// @generated from file middleware/v1/match.proto (package sentinel.v1, syntax proto3) +// @generated from file policies/v1/match.proto (package sentinel.v1, syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; @@ -7,10 +7,10 @@ import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** - * Describes the file middleware/v1/match.proto. + * Describes the file policies/v1/match.proto. */ -export const file_middleware_v1_match: GenFile = /*@__PURE__*/ - fileDesc("ChltaWRkbGV3YXJlL3YxL21hdGNoLnByb3RvEgtzZW50aW5lbC52MSLIAQoJTWF0Y2hFeHByEiYKBHBhdGgYASABKAsyFi5zZW50aW5lbC52MS5QYXRoTWF0Y2hIABIqCgZtZXRob2QYAiABKAsyGC5zZW50aW5lbC52MS5NZXRob2RNYXRjaEgAEioKBmhlYWRlchgDIAEoCzIYLnNlbnRpbmVsLnYxLkhlYWRlck1hdGNoSAASMwoLcXVlcnlfcGFyYW0YBCABKAsyHC5zZW50aW5lbC52MS5RdWVyeVBhcmFtTWF0Y2hIAEIGCgRleHByIl8KC1N0cmluZ01hdGNoEhMKC2lnbm9yZV9jYXNlGAEgASgIEg8KBWV4YWN0GAIgASgJSAASEAoGcHJlZml4GAMgASgJSAASDwoFcmVnZXgYBCABKAlIAEIHCgVtYXRjaCIzCglQYXRoTWF0Y2gSJgoEcGF0aBgBIAEoCzIYLnNlbnRpbmVsLnYxLlN0cmluZ01hdGNoIh4KC01ldGhvZE1hdGNoEg8KB21ldGhvZHMYASADKAkiYgoLSGVhZGVyTWF0Y2gSDAoEbmFtZRgBIAEoCRIRCgdwcmVzZW50GAIgASgISAASKQoFdmFsdWUYAyABKAsyGC5zZW50aW5lbC52MS5TdHJpbmdNYXRjaEgAQgcKBW1hdGNoImYKD1F1ZXJ5UGFyYW1NYXRjaBIMCgRuYW1lGAEgASgJEhEKB3ByZXNlbnQYAiABKAhIABIpCgV2YWx1ZRgDIAEoCzIYLnNlbnRpbmVsLnYxLlN0cmluZ01hdGNoSABCBwoFbWF0Y2hCpQEKD2NvbS5zZW50aW5lbC52MUIKTWF0Y2hQcm90b1ABWjlnaXRodWIuY29tL3Vua2V5ZWQvdW5rZXkvZ2VuL3Byb3RvL3NlbnRpbmVsL3YxO3NlbnRpbmVsdjGiAgNTWFiqAgtTZW50aW5lbC5WMcoCC1NlbnRpbmVsXFYx4gIXU2VudGluZWxcVjFcR1BCTWV0YWRhdGHqAgxTZW50aW5lbDo6VjFiBnByb3RvMw"); +export const file_policies_v1_match: GenFile = /*@__PURE__*/ + fileDesc("Chdwb2xpY2llcy92MS9tYXRjaC5wcm90bxILc2VudGluZWwudjEiyAEKCU1hdGNoRXhwchImCgRwYXRoGAEgASgLMhYuc2VudGluZWwudjEuUGF0aE1hdGNoSAASKgoGbWV0aG9kGAIgASgLMhguc2VudGluZWwudjEuTWV0aG9kTWF0Y2hIABIqCgZoZWFkZXIYAyABKAsyGC5zZW50aW5lbC52MS5IZWFkZXJNYXRjaEgAEjMKC3F1ZXJ5X3BhcmFtGAQgASgLMhwuc2VudGluZWwudjEuUXVlcnlQYXJhbU1hdGNoSABCBgoEZXhwciJfCgtTdHJpbmdNYXRjaBITCgtpZ25vcmVfY2FzZRgBIAEoCBIPCgVleGFjdBgCIAEoCUgAEhAKBnByZWZpeBgDIAEoCUgAEg8KBXJlZ2V4GAQgASgJSABCBwoFbWF0Y2giMwoJUGF0aE1hdGNoEiYKBHBhdGgYASABKAsyGC5zZW50aW5lbC52MS5TdHJpbmdNYXRjaCIeCgtNZXRob2RNYXRjaBIPCgdtZXRob2RzGAEgAygJImIKC0hlYWRlck1hdGNoEgwKBG5hbWUYASABKAkSEQoHcHJlc2VudBgCIAEoCEgAEikKBXZhbHVlGAMgASgLMhguc2VudGluZWwudjEuU3RyaW5nTWF0Y2hIAEIHCgVtYXRjaCJmCg9RdWVyeVBhcmFtTWF0Y2gSDAoEbmFtZRgBIAEoCRIRCgdwcmVzZW50GAIgASgISAASKQoFdmFsdWUYAyABKAsyGC5zZW50aW5lbC52MS5TdHJpbmdNYXRjaEgAQgcKBW1hdGNoQqUBCg9jb20uc2VudGluZWwudjFCCk1hdGNoUHJvdG9QAVo5Z2l0aHViLmNvbS91bmtleWVkL3Vua2V5L2dlbi9wcm90by9zZW50aW5lbC92MTtzZW50aW5lbHYxogIDU1hYqgILU2VudGluZWwuVjHKAgtTZW50aW5lbFxWMeICF1NlbnRpbmVsXFYxXEdQQk1ldGFkYXRh6gIMU2VudGluZWw6OlYxYgZwcm90bzM"); /** * MatchExpr tests a single property of an incoming HTTP request. @@ -62,7 +62,7 @@ export type MatchExpr = Message<"sentinel.v1.MatchExpr"> & { * Use `create(MatchExprSchema)` to create a new message. */ export const MatchExprSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_match, 0); + messageDesc(file_policies_v1_match, 0); /** * StringMatch is the shared string matching primitive used by all leaf @@ -124,7 +124,7 @@ export type StringMatch = Message<"sentinel.v1.StringMatch"> & { * Use `create(StringMatchSchema)` to create a new message. */ export const StringMatchSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_match, 1); + messageDesc(file_policies_v1_match, 1); /** * PathMatch tests the URL path of the incoming request. The path is compared @@ -146,7 +146,7 @@ export type PathMatch = Message<"sentinel.v1.PathMatch"> & { * Use `create(PathMatchSchema)` to create a new message. */ export const PathMatchSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_match, 2); + messageDesc(file_policies_v1_match, 2); /** * MethodMatch tests the HTTP method of the incoming request. Comparison is @@ -171,7 +171,7 @@ export type MethodMatch = Message<"sentinel.v1.MethodMatch"> & { * Use `create(MethodMatchSchema)` to create a new message. */ export const MethodMatchSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_match, 3); + messageDesc(file_policies_v1_match, 3); /** * HeaderMatch tests a request header by name and optionally by value. Header @@ -226,7 +226,7 @@ export type HeaderMatch = Message<"sentinel.v1.HeaderMatch"> & { * Use `create(HeaderMatchSchema)` to create a new message. */ export const HeaderMatchSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_match, 4); + messageDesc(file_policies_v1_match, 4); /** * QueryParamMatch tests a URL query parameter by name and optionally by @@ -276,5 +276,5 @@ export type QueryParamMatch = Message<"sentinel.v1.QueryParamMatch"> & { * Use `create(QueryParamMatchSchema)` to create a new message. */ export const QueryParamMatchSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_match, 5); + messageDesc(file_policies_v1_match, 5); diff --git a/web/apps/dashboard/gen/proto/middleware/v1/openapi_pb.ts b/web/apps/dashboard/gen/proto/policies/v1/openapi_pb.ts similarity index 78% rename from web/apps/dashboard/gen/proto/middleware/v1/openapi_pb.ts rename to web/apps/dashboard/gen/proto/policies/v1/openapi_pb.ts index 6f301a7a6e..cb3ea5a8d8 100644 --- a/web/apps/dashboard/gen/proto/middleware/v1/openapi_pb.ts +++ b/web/apps/dashboard/gen/proto/policies/v1/openapi_pb.ts @@ -1,5 +1,5 @@ // @generated by protoc-gen-es v2.8.0 with parameter "target=ts" -// @generated from file middleware/v1/openapi.proto (package sentinel.v1, syntax proto3) +// @generated from file policies/v1/openapi.proto (package sentinel.v1, syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; @@ -7,10 +7,10 @@ import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** - * Describes the file middleware/v1/openapi.proto. + * Describes the file policies/v1/openapi.proto. */ -export const file_middleware_v1_openapi: GenFile = /*@__PURE__*/ - fileDesc("ChttaWRkbGV3YXJlL3YxL29wZW5hcGkucHJvdG8SC3NlbnRpbmVsLnYxIi0KGE9wZW5BcGlSZXF1ZXN0VmFsaWRhdGlvbhIRCglzcGVjX3lhbWwYASABKAxCpwEKD2NvbS5zZW50aW5lbC52MUIMT3BlbmFwaVByb3RvUAFaOWdpdGh1Yi5jb20vdW5rZXllZC91bmtleS9nZW4vcHJvdG8vc2VudGluZWwvdjE7c2VudGluZWx2MaICA1NYWKoCC1NlbnRpbmVsLlYxygILU2VudGluZWxcVjHiAhdTZW50aW5lbFxWMVxHUEJNZXRhZGF0YeoCDFNlbnRpbmVsOjpWMWIGcHJvdG8z"); +export const file_policies_v1_openapi: GenFile = /*@__PURE__*/ + fileDesc("Chlwb2xpY2llcy92MS9vcGVuYXBpLnByb3RvEgtzZW50aW5lbC52MSItChhPcGVuQXBpUmVxdWVzdFZhbGlkYXRpb24SEQoJc3BlY195YW1sGAEgASgMQqcBCg9jb20uc2VudGluZWwudjFCDE9wZW5hcGlQcm90b1ABWjlnaXRodWIuY29tL3Vua2V5ZWQvdW5rZXkvZ2VuL3Byb3RvL3NlbnRpbmVsL3YxO3NlbnRpbmVsdjGiAgNTWFiqAgtTZW50aW5lbC5WMcoCC1NlbnRpbmVsXFYx4gIXU2VudGluZWxcVjFcR1BCTWV0YWRhdGHqAgxTZW50aW5lbDo6VjFiBnByb3RvMw"); /** * OpenApiRequestValidation validates incoming HTTP requests against an OpenAPI @@ -54,5 +54,5 @@ export type OpenApiRequestValidation = Message<"sentinel.v1.OpenApiRequestValida * Use `create(OpenApiRequestValidationSchema)` to create a new message. */ export const OpenApiRequestValidationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_openapi, 0); + messageDesc(file_policies_v1_openapi, 0); diff --git a/web/apps/dashboard/gen/proto/policies/v1/policy_pb.ts b/web/apps/dashboard/gen/proto/policies/v1/policy_pb.ts new file mode 100644 index 0000000000..a6f5e49477 --- /dev/null +++ b/web/apps/dashboard/gen/proto/policies/v1/policy_pb.ts @@ -0,0 +1,135 @@ +// @generated by protoc-gen-es v2.8.0 with parameter "target=ts" +// @generated from file policies/v1/policy.proto (package sentinel.v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { BasicAuth } from "./basicauth_pb"; +import { file_policies_v1_basicauth } from "./basicauth_pb"; +import type { IPRules } from "./iprules_pb"; +import { file_policies_v1_iprules } from "./iprules_pb"; +import type { JWTAuth } from "./jwtauth_pb"; +import { file_policies_v1_jwtauth } from "./jwtauth_pb"; +import type { KeyAuth } from "./keyauth_pb"; +import { file_policies_v1_keyauth } from "./keyauth_pb"; +import type { MatchExpr } from "./match_pb"; +import { file_policies_v1_match } from "./match_pb"; +import type { OpenApiRequestValidation } from "./openapi_pb"; +import { file_policies_v1_openapi } from "./openapi_pb"; +import type { RateLimit } from "./ratelimit_pb"; +import { file_policies_v1_ratelimit } from "./ratelimit_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file policies/v1/policy.proto. + */ +export const file_policies_v1_policy: GenFile = /*@__PURE__*/ + fileDesc("Chhwb2xpY2llcy92MS9wb2xpY3kucHJvdG8SC3NlbnRpbmVsLnYxIvQCCgZQb2xpY3kSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIPCgdlbmFibGVkGAMgASgIEiUKBW1hdGNoGAQgAygLMhYuc2VudGluZWwudjEuTWF0Y2hFeHByEicKB2tleWF1dGgYBSABKAsyFC5zZW50aW5lbC52MS5LZXlBdXRoSAASJwoHand0YXV0aBgGIAEoCzIULnNlbnRpbmVsLnYxLkpXVEF1dGhIABIrCgliYXNpY2F1dGgYByABKAsyFi5zZW50aW5lbC52MS5CYXNpY0F1dGhIABIrCglyYXRlbGltaXQYCCABKAsyFi5zZW50aW5lbC52MS5SYXRlTGltaXRIABIoCghpcF9ydWxlcxgJIAEoCzIULnNlbnRpbmVsLnYxLklQUnVsZXNIABI4CgdvcGVuYXBpGAogASgLMiUuc2VudGluZWwudjEuT3BlbkFwaVJlcXVlc3RWYWxpZGF0aW9uSABCCAoGY29uZmlnQqYBCg9jb20uc2VudGluZWwudjFCC1BvbGljeVByb3RvUAFaOWdpdGh1Yi5jb20vdW5rZXllZC91bmtleS9nZW4vcHJvdG8vc2VudGluZWwvdjE7c2VudGluZWx2MaICA1NYWKoCC1NlbnRpbmVsLlYxygILU2VudGluZWxcVjHiAhdTZW50aW5lbFxWMVxHUEJNZXRhZGF0YeoCDFNlbnRpbmVsOjpWMWIGcHJvdG8z", [file_policies_v1_basicauth, file_policies_v1_iprules, file_policies_v1_jwtauth, file_policies_v1_keyauth, file_policies_v1_match, file_policies_v1_openapi, file_policies_v1_ratelimit]); + +/** + * Policy is a single middleware layer in a deployment's configuration. Each policy + * combines a match expression (which requests does it apply to?) with a + * configuration (what does it do?). This separation is what makes the system + * composable: the same rate limiter config can be scoped to POST /api/* + * without the rate limiter needing to know anything about path matching. + * + * Policies carry a stable id for correlation across logs, metrics, and + * debugging. The disabled flag allows operators to disable a policy without + * removing it from config, which is critical for incident response — you can + * turn off a misbehaving policy and re-enable it once the issue is resolved, + * without losing the configuration or triggering a full redeploy. + * + * @generated from message sentinel.v1.Policy + */ +export type Policy = Message<"sentinel.v1.Policy"> & { + /** + * Stable identifier for this policy, used in log entries, metrics labels, + * and error messages. Should be unique within a deployment's Middleware + * config. Typically a UUID or a slug like "api-ratelimit". + * + * @generated from field: string id = 1; + */ + id: string; + + /** + * Human-friendly label displayed in the dashboard and audit logs. + * Does not affect policy behavior. + * + * @generated from field: string name = 2; + */ + name: string; + + /** + * When false, sentinel skips this policy entirely during evaluation. + * This allows operators to toggle policies on and off without modifying + * or removing the underlying configuration, which is useful during + * incidents, gradual rollouts, and debugging. + * + * @generated from field: bool enabled = 3; + */ + enabled: boolean; + + /** + * Match conditions that determine which requests this policy applies to. + * All entries must match for the policy to run (implicit AND). An empty + * list matches all requests — this is the common case for global policies + * like IP allowlists or rate limiting. + * + * For OR semantics, create separate policies with the same config and + * different match lists. + * + * @generated from field: repeated sentinel.v1.MatchExpr match = 4; + */ + match: MatchExpr[]; + + /** + * The policy configuration. Exactly one must be set. + * + * @generated from oneof sentinel.v1.Policy.config + */ + config: { + /** + * @generated from field: sentinel.v1.KeyAuth keyauth = 5; + */ + value: KeyAuth; + case: "keyauth"; + } | { + /** + * @generated from field: sentinel.v1.JWTAuth jwtauth = 6; + */ + value: JWTAuth; + case: "jwtauth"; + } | { + /** + * @generated from field: sentinel.v1.BasicAuth basicauth = 7; + */ + value: BasicAuth; + case: "basicauth"; + } | { + /** + * @generated from field: sentinel.v1.RateLimit ratelimit = 8; + */ + value: RateLimit; + case: "ratelimit"; + } | { + /** + * @generated from field: sentinel.v1.IPRules ip_rules = 9; + */ + value: IPRules; + case: "ipRules"; + } | { + /** + * @generated from field: sentinel.v1.OpenApiRequestValidation openapi = 10; + */ + value: OpenApiRequestValidation; + case: "openapi"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message sentinel.v1.Policy. + * Use `create(PolicySchema)` to create a new message. + */ +export const PolicySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_policies_v1_policy, 0); + diff --git a/web/apps/dashboard/gen/proto/middleware/v1/principal_pb.ts b/web/apps/dashboard/gen/proto/policies/v1/principal_pb.ts similarity index 80% rename from web/apps/dashboard/gen/proto/middleware/v1/principal_pb.ts rename to web/apps/dashboard/gen/proto/policies/v1/principal_pb.ts index e367608626..19dd192d0d 100644 --- a/web/apps/dashboard/gen/proto/middleware/v1/principal_pb.ts +++ b/web/apps/dashboard/gen/proto/policies/v1/principal_pb.ts @@ -1,5 +1,5 @@ // @generated by protoc-gen-es v2.8.0 with parameter "target=ts" -// @generated from file middleware/v1/principal.proto (package sentinel.v1, syntax proto3) +// @generated from file policies/v1/principal.proto (package sentinel.v1, syntax proto3) /* eslint-disable */ import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; @@ -7,10 +7,10 @@ import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** - * Describes the file middleware/v1/principal.proto. + * Describes the file policies/v1/principal.proto. */ -export const file_middleware_v1_principal: GenFile = /*@__PURE__*/ - fileDesc("Ch1taWRkbGV3YXJlL3YxL3ByaW5jaXBhbC5wcm90bxILc2VudGluZWwudjEiqQEKCVByaW5jaXBhbBIPCgdzdWJqZWN0GAEgASgJEigKBHR5cGUYAiABKA4yGi5zZW50aW5lbC52MS5QcmluY2lwYWxUeXBlEjIKBmNsYWltcxgDIAMoCzIiLnNlbnRpbmVsLnYxLlByaW5jaXBhbC5DbGFpbXNFbnRyeRotCgtDbGFpbXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBKn0KDVByaW5jaXBhbFR5cGUSHgoaUFJJTkNJUEFMX1RZUEVfVU5TUEVDSUZJRUQQABIaChZQUklOQ0lQQUxfVFlQRV9BUElfS0VZEAESFgoSUFJJTkNJUEFMX1RZUEVfSldUEAISGAoUUFJJTkNJUEFMX1RZUEVfQkFTSUMQA0KpAQoPY29tLnNlbnRpbmVsLnYxQg5QcmluY2lwYWxQcm90b1ABWjlnaXRodWIuY29tL3Vua2V5ZWQvdW5rZXkvZ2VuL3Byb3RvL3NlbnRpbmVsL3YxO3NlbnRpbmVsdjGiAgNTWFiqAgtTZW50aW5lbC5WMcoCC1NlbnRpbmVsXFYx4gIXU2VudGluZWxcVjFcR1BCTWV0YWRhdGHqAgxTZW50aW5lbDo6VjFiBnByb3RvMw"); +export const file_policies_v1_principal: GenFile = /*@__PURE__*/ + fileDesc("Chtwb2xpY2llcy92MS9wcmluY2lwYWwucHJvdG8SC3NlbnRpbmVsLnYxIqkBCglQcmluY2lwYWwSDwoHc3ViamVjdBgBIAEoCRIoCgR0eXBlGAIgASgOMhouc2VudGluZWwudjEuUHJpbmNpcGFsVHlwZRIyCgZjbGFpbXMYAyADKAsyIi5zZW50aW5lbC52MS5QcmluY2lwYWwuQ2xhaW1zRW50cnkaLQoLQ2xhaW1zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASp9Cg1QcmluY2lwYWxUeXBlEh4KGlBSSU5DSVBBTF9UWVBFX1VOU1BFQ0lGSUVEEAASGgoWUFJJTkNJUEFMX1RZUEVfQVBJX0tFWRABEhYKElBSSU5DSVBBTF9UWVBFX0pXVBACEhgKFFBSSU5DSVBBTF9UWVBFX0JBU0lDEANCqQEKD2NvbS5zZW50aW5lbC52MUIOUHJpbmNpcGFsUHJvdG9QAVo5Z2l0aHViLmNvbS91bmtleWVkL3Vua2V5L2dlbi9wcm90by9zZW50aW5lbC92MTtzZW50aW5lbHYxogIDU1hYqgILU2VudGluZWwuVjHKAgtTZW50aW5lbFxWMeICF1NlbnRpbmVsXFYxXEdQQk1ldGFkYXRh6gIMU2VudGluZWw6OlYxYgZwcm90bzM"); /** * Principal is the authenticated entity produced by any authentication policy. @@ -81,7 +81,7 @@ export type Principal = Message<"sentinel.v1.Principal"> & { * Use `create(PrincipalSchema)` to create a new message. */ export const PrincipalSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_principal, 0); + messageDesc(file_policies_v1_principal, 0); /** * PrincipalType identifies which authentication method produced a [Principal]. @@ -121,5 +121,5 @@ export enum PrincipalType { * Describes the enum sentinel.v1.PrincipalType. */ export const PrincipalTypeSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_middleware_v1_principal, 0); + enumDesc(file_policies_v1_principal, 0); diff --git a/web/apps/dashboard/gen/proto/middleware/v1/ratelimit_pb.ts b/web/apps/dashboard/gen/proto/policies/v1/ratelimit_pb.ts similarity index 84% rename from web/apps/dashboard/gen/proto/middleware/v1/ratelimit_pb.ts rename to web/apps/dashboard/gen/proto/policies/v1/ratelimit_pb.ts index 69f8974c6b..19d4101d0a 100644 --- a/web/apps/dashboard/gen/proto/middleware/v1/ratelimit_pb.ts +++ b/web/apps/dashboard/gen/proto/policies/v1/ratelimit_pb.ts @@ -1,5 +1,5 @@ // @generated by protoc-gen-es v2.8.0 with parameter "target=ts" -// @generated from file middleware/v1/ratelimit.proto (package sentinel.v1, syntax proto3) +// @generated from file policies/v1/ratelimit.proto (package sentinel.v1, syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; @@ -7,10 +7,10 @@ import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** - * Describes the file middleware/v1/ratelimit.proto. + * Describes the file policies/v1/ratelimit.proto. */ -export const file_middleware_v1_ratelimit: GenFile = /*@__PURE__*/ - fileDesc("Ch1taWRkbGV3YXJlL3YxL3JhdGVsaW1pdC5wcm90bxILc2VudGluZWwudjEiVQoJUmF0ZUxpbWl0Eg0KBWxpbWl0GAEgASgDEhEKCXdpbmRvd19tcxgCIAEoAxImCgNrZXkYAyABKAsyGS5zZW50aW5lbC52MS5SYXRlTGltaXRLZXkimQIKDFJhdGVMaW1pdEtleRItCglyZW1vdGVfaXAYASABKAsyGC5zZW50aW5lbC52MS5SZW1vdGVJcEtleUgAEigKBmhlYWRlchgCIAEoCzIWLnNlbnRpbmVsLnYxLkhlYWRlcktleUgAEkUKFWF1dGhlbnRpY2F0ZWRfc3ViamVjdBgDIAEoCzIkLnNlbnRpbmVsLnYxLkF1dGhlbnRpY2F0ZWRTdWJqZWN0S2V5SAASJAoEcGF0aBgEIAEoCzIULnNlbnRpbmVsLnYxLlBhdGhLZXlIABI5Cg9wcmluY2lwYWxfY2xhaW0YBSABKAsyHi5zZW50aW5lbC52MS5QcmluY2lwYWxDbGFpbUtleUgAQggKBnNvdXJjZSINCgtSZW1vdGVJcEtleSIZCglIZWFkZXJLZXkSDAoEbmFtZRgBIAEoCSIZChdBdXRoZW50aWNhdGVkU3ViamVjdEtleSIJCgdQYXRoS2V5IicKEVByaW5jaXBhbENsYWltS2V5EhIKCmNsYWltX25hbWUYASABKAlCqQEKD2NvbS5zZW50aW5lbC52MUIOUmF0ZWxpbWl0UHJvdG9QAVo5Z2l0aHViLmNvbS91bmtleWVkL3Vua2V5L2dlbi9wcm90by9zZW50aW5lbC92MTtzZW50aW5lbHYxogIDU1hYqgILU2VudGluZWwuVjHKAgtTZW50aW5lbFxWMeICF1NlbnRpbmVsXFYxXEdQQk1ldGFkYXRh6gIMU2VudGluZWw6OlYxYgZwcm90bzM"); +export const file_policies_v1_ratelimit: GenFile = /*@__PURE__*/ + fileDesc("Chtwb2xpY2llcy92MS9yYXRlbGltaXQucHJvdG8SC3NlbnRpbmVsLnYxIlUKCVJhdGVMaW1pdBINCgVsaW1pdBgBIAEoAxIRCgl3aW5kb3dfbXMYAiABKAMSJgoDa2V5GAMgASgLMhkuc2VudGluZWwudjEuUmF0ZUxpbWl0S2V5IpkCCgxSYXRlTGltaXRLZXkSLQoJcmVtb3RlX2lwGAEgASgLMhguc2VudGluZWwudjEuUmVtb3RlSXBLZXlIABIoCgZoZWFkZXIYAiABKAsyFi5zZW50aW5lbC52MS5IZWFkZXJLZXlIABJFChVhdXRoZW50aWNhdGVkX3N1YmplY3QYAyABKAsyJC5zZW50aW5lbC52MS5BdXRoZW50aWNhdGVkU3ViamVjdEtleUgAEiQKBHBhdGgYBCABKAsyFC5zZW50aW5lbC52MS5QYXRoS2V5SAASOQoPcHJpbmNpcGFsX2NsYWltGAUgASgLMh4uc2VudGluZWwudjEuUHJpbmNpcGFsQ2xhaW1LZXlIAEIICgZzb3VyY2UiDQoLUmVtb3RlSXBLZXkiGQoJSGVhZGVyS2V5EgwKBG5hbWUYASABKAkiGQoXQXV0aGVudGljYXRlZFN1YmplY3RLZXkiCQoHUGF0aEtleSInChFQcmluY2lwYWxDbGFpbUtleRISCgpjbGFpbV9uYW1lGAEgASgJQqkBCg9jb20uc2VudGluZWwudjFCDlJhdGVsaW1pdFByb3RvUAFaOWdpdGh1Yi5jb20vdW5rZXllZC91bmtleS9nZW4vcHJvdG8vc2VudGluZWwvdjE7c2VudGluZWx2MaICA1NYWKoCC1NlbnRpbmVsLlYxygILU2VudGluZWxcVjHiAhdTZW50aW5lbFxWMVxHUEJNZXRhZGF0YeoCDFNlbnRpbmVsOjpWMWIGcHJvdG8z"); /** * RateLimit enforces request rate limits at the gateway, protecting upstream @@ -66,7 +66,7 @@ export type RateLimit = Message<"sentinel.v1.RateLimit"> & { * Use `create(RateLimitSchema)` to create a new message. */ export const RateLimitSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_ratelimit, 0); + messageDesc(file_policies_v1_ratelimit, 0); /** * RateLimitKey determines how sentinel identifies the entity being rate @@ -147,7 +147,7 @@ export type RateLimitKey = Message<"sentinel.v1.RateLimitKey"> & { * Use `create(RateLimitKeySchema)` to create a new message. */ export const RateLimitKeySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_ratelimit, 1); + messageDesc(file_policies_v1_ratelimit, 1); /** * RemoteIpKey derives the rate limit key from the client's IP address. @@ -162,7 +162,7 @@ export type RemoteIpKey = Message<"sentinel.v1.RemoteIpKey"> & { * Use `create(RemoteIpKeySchema)` to create a new message. */ export const RemoteIpKeySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_ratelimit, 2); + messageDesc(file_policies_v1_ratelimit, 2); /** * HeaderKey derives the rate limit key from a request header value. @@ -184,7 +184,7 @@ export type HeaderKey = Message<"sentinel.v1.HeaderKey"> & { * Use `create(HeaderKeySchema)` to create a new message. */ export const HeaderKeySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_ratelimit, 3); + messageDesc(file_policies_v1_ratelimit, 3); /** * AuthenticatedSubjectKey derives the rate limit key from the [Principal] @@ -202,7 +202,7 @@ export type AuthenticatedSubjectKey = Message<"sentinel.v1.AuthenticatedSubjectK * Use `create(AuthenticatedSubjectKeySchema)` to create a new message. */ export const AuthenticatedSubjectKeySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_ratelimit, 4); + messageDesc(file_policies_v1_ratelimit, 4); /** * PathKey derives the rate limit key from the request URL path. @@ -217,7 +217,7 @@ export type PathKey = Message<"sentinel.v1.PathKey"> & { * Use `create(PathKeySchema)` to create a new message. */ export const PathKeySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_ratelimit, 5); + messageDesc(file_policies_v1_ratelimit, 5); /** * PrincipalClaimKey derives the rate limit key from a named claim in the @@ -241,5 +241,5 @@ export type PrincipalClaimKey = Message<"sentinel.v1.PrincipalClaimKey"> & { * Use `create(PrincipalClaimKeySchema)` to create a new message. */ export const PrincipalClaimKeySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_middleware_v1_ratelimit, 6); + messageDesc(file_policies_v1_ratelimit, 6); diff --git a/web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/get-available-keyspaces.ts b/web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/get-available-keyspaces.ts new file mode 100644 index 0000000000..f1be251382 --- /dev/null +++ b/web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/get-available-keyspaces.ts @@ -0,0 +1,26 @@ +import { db } from "@/lib/db"; +import { workspaceProcedure } from "../../../trpc"; + +export const getAvailableKeyspaces = workspaceProcedure.query(async ({ ctx }) => { + const keyspaces = await db.query.keyAuth.findMany({ + where: (table, { eq }) => eq(table.workspaceId, ctx.workspace.id), + columns: { + id: true, + }, + with: { + api: { + columns: { + name: true, + }, + }, + }, + }); + + return keyspaces.reduce( + (acc, ks) => { + acc[ks.id] = ks; + return acc; + }, + {} as Record, + ); +}); diff --git a/web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/get.ts b/web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/get.ts index f56af0d159..d55f794e83 100644 --- a/web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/get.ts +++ b/web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/get.ts @@ -1,3 +1,4 @@ +import type { Config } from "@/gen/proto/config/v1/config_pb"; import { and, db, eq } from "@/lib/db"; import { environmentBuildSettings, environmentRuntimeSettings } from "@unkey/db/src/schema"; import { z } from "zod"; @@ -21,5 +22,15 @@ export const getEnvironmentSettings = workspaceProcedure }), ]); - return { buildSettings: buildSettings ?? null, runtimeSettings: runtimeSettings ?? null }; + return { + buildSettings: buildSettings ?? null, + runtimeSettings: runtimeSettings + ? { + ...runtimeSettings, + sentinelConfig: runtimeSettings.sentinelConfig + ? (JSON.parse(Buffer.from(runtimeSettings.sentinelConfig).toString()) as Config) + : undefined, + } + : null, + }; }); diff --git a/web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/sentinel/update-middleware.ts b/web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/sentinel/update-middleware.ts new file mode 100644 index 0000000000..595c61cba7 --- /dev/null +++ b/web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/sentinel/update-middleware.ts @@ -0,0 +1,66 @@ +import { and, db, eq } from "@/lib/db"; +import { TRPCError } from "@trpc/server"; +import { environmentRuntimeSettings } from "@unkey/db/src/schema"; +import { z } from "zod"; +import { workspaceProcedure } from "../../../../trpc"; + +// This is 100% not how we will do it later and is just a shortcut to use keyspace middleware before building the actual UI for it. +export const updateMiddleware = workspaceProcedure + .input( + z.object({ + environmentId: z.string(), + keyspaceIds: z.array(z.string()).max(10), + }), + ) + .mutation(async ({ ctx, input }) => { + const sentinelConfig: { + policies: { + id: string; + name: string; + enabled: boolean; + keyauth: { keySpaceIds: string[] }; + }[]; + } = { + policies: [], + }; + if (input.keyspaceIds.length > 0) { + const keyspaces = await db.query.keyAuth + .findMany({ + where: (table, { and, inArray }) => + and(inArray(table.id, input.keyspaceIds), eq(table.workspaceId, ctx.workspace.id)), + columns: { id: true }, + }) + .catch((err) => { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "unable to load keyspaces", + }); + }); + + for (const id of input.keyspaceIds) { + if (!keyspaces.find((ks) => ks.id === id)) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `keyspace ${id} does not exist`, + }); + } + } + + sentinelConfig.policies.push({ + id: "keyauth-policy", + name: "API Key Auth", + enabled: true, + keyauth: { keySpaceIds: ["ks_NNh4XwVsZiwG"] }, + }); + } + await db + .update(environmentRuntimeSettings) + .set({ sentinelConfig: JSON.stringify(sentinelConfig) }) + .where( + and( + eq(environmentRuntimeSettings.workspaceId, ctx.workspace.id), + eq(environmentRuntimeSettings.environmentId, input.environmentId), + ), + ); + }); diff --git a/web/apps/dashboard/lib/trpc/routers/index.ts b/web/apps/dashboard/lib/trpc/routers/index.ts index b15b9675d0..1c72f2a339 100644 --- a/web/apps/dashboard/lib/trpc/routers/index.ts +++ b/web/apps/dashboard/lib/trpc/routers/index.ts @@ -58,6 +58,7 @@ import { updateEnvVar } from "./deploy/env-vars/update"; import { updateDockerContext } from "./deploy/environment-settings/build/update-docker-context"; import { updateDockerfile } from "./deploy/environment-settings/build/update-dockerfile"; import { getEnvironmentSettings } from "./deploy/environment-settings/get"; +import { getAvailableKeyspaces } from "./deploy/environment-settings/get-available-keyspaces"; import { getAvailableRegions } from "./deploy/environment-settings/get-available-regions"; import { updateCommand } from "./deploy/environment-settings/runtime/update-command"; import { updateCpu } from "./deploy/environment-settings/runtime/update-cpu"; @@ -66,6 +67,7 @@ import { updateInstances } from "./deploy/environment-settings/runtime/update-in import { updateMemory } from "./deploy/environment-settings/runtime/update-memory"; import { updatePort } from "./deploy/environment-settings/runtime/update-port"; import { updateRegions } from "./deploy/environment-settings/runtime/update-regions"; +import { updateMiddleware } from "./deploy/environment-settings/sentinel/update-middleware"; import { getDeploymentLatency } from "./deploy/metrics/get-deployment-latency"; import { getDeploymentLatencyTimeseries } from "./deploy/metrics/get-deployment-latency-timeseries"; import { getDeploymentRps } from "./deploy/metrics/get-deployment-rps"; @@ -404,6 +406,10 @@ export const router = t.router({ environmentSettings: t.router({ get: getEnvironmentSettings, getAvailableRegions, + getAvailableKeyspaces, + sentinel: t.router({ + updateMiddleware, + }), runtime: t.router({ updateCpu, updateMemory, diff --git a/web/internal/db/src/schema/environment_runtime_settings.ts b/web/internal/db/src/schema/environment_runtime_settings.ts index f0b4bf1a14..190c06fa9e 100644 --- a/web/internal/db/src/schema/environment_runtime_settings.ts +++ b/web/internal/db/src/schema/environment_runtime_settings.ts @@ -10,6 +10,7 @@ import { } from "drizzle-orm/mysql-core"; import { environments } from "./environments"; import { lifecycleDates } from "./util/lifecycle_dates"; +import { longblob } from "./util/longblob"; import { workspaces } from "./workspaces"; export type Healthcheck = { @@ -48,6 +49,8 @@ export const environmentRuntimeSettings = mysqlTable( .notNull() .default("SIGTERM"), + sentinelConfig: longblob("sentinel_config"), + ...lifecycleDates, }, (table) => [uniqueIndex("env_runtime_settings_environment_id_idx").on(table.environmentId)], diff --git a/web/internal/db/src/schema/environments.ts b/web/internal/db/src/schema/environments.ts index c61ed83e0d..c942fe56c7 100644 --- a/web/internal/db/src/schema/environments.ts +++ b/web/internal/db/src/schema/environments.ts @@ -18,6 +18,8 @@ export const environments = mysqlTable( slug: varchar("slug", { length: 256 }).notNull(), // URL-safe identifier within workspace description: varchar("description", { length: 255 }).notNull().default(""), + // @deprecated + // use environment_runtime_settings.sentinel_config instead sentinelConfig: longblob("sentinel_config").notNull(), ...deleteProtection, From 03f8584357e9d643742044a94ab5d718db5a1ddf Mon Sep 17 00:00:00 2001 From: Andreas Thomas Date: Thu, 19 Feb 2026 16:30:41 +0100 Subject: [PATCH 5/8] Update svc/sentinel/engine/match.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- svc/sentinel/engine/match.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/svc/sentinel/engine/match.go b/svc/sentinel/engine/match.go index 5ff401de61..58c8898024 100644 --- a/svc/sentinel/engine/match.go +++ b/svc/sentinel/engine/match.go @@ -58,6 +58,9 @@ func matchesRequest(req *http.Request, exprs []*sentinelv1.MatchExpr, rc *regexC } func evalMatchExpr(req *http.Request, expr *sentinelv1.MatchExpr, rc *regexCache) (bool, error) { + if expr == nil { + return true, nil + } switch e := expr.GetExpr().(type) { case *sentinelv1.MatchExpr_Path: return evalPathMatch(req, e.Path, rc) @@ -71,6 +74,7 @@ func evalMatchExpr(req *http.Request, expr *sentinelv1.MatchExpr, rc *regexCache return false, nil } } +} func evalPathMatch(req *http.Request, pm *sentinelv1.PathMatch, rc *regexCache) (bool, error) { if pm == nil || pm.GetPath() == nil { From 81ef110d947f1a6ed3ac05e733fa991606918e64 Mon Sep 17 00:00:00 2001 From: chronark Date: Thu, 19 Feb 2026 16:33:52 +0100 Subject: [PATCH 6/8] fix: coderabbit --- svc/sentinel/engine/match.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/svc/sentinel/engine/match.go b/svc/sentinel/engine/match.go index 58c8898024..05adda5537 100644 --- a/svc/sentinel/engine/match.go +++ b/svc/sentinel/engine/match.go @@ -59,7 +59,7 @@ func matchesRequest(req *http.Request, exprs []*sentinelv1.MatchExpr, rc *regexC func evalMatchExpr(req *http.Request, expr *sentinelv1.MatchExpr, rc *regexCache) (bool, error) { if expr == nil { - return true, nil + return false, nil } switch e := expr.GetExpr().(type) { case *sentinelv1.MatchExpr_Path: @@ -73,7 +73,7 @@ func evalMatchExpr(req *http.Request, expr *sentinelv1.MatchExpr, rc *regexCache default: return false, nil } -} + } func evalPathMatch(req *http.Request, pm *sentinelv1.PathMatch, rc *regexCache) (bool, error) { From f03e2f60ee8cfefe89d07c0b18746052cb39c8cd Mon Sep 17 00:00:00 2001 From: chronark Date: Thu, 19 Feb 2026 17:00:16 +0100 Subject: [PATCH 7/8] chore: clean up old columns --- cmd/dev/seed/local.go | 66 +++++++++---------- .../bulk_environment_insert.sql_generated.go | 5 +- ...t_runtime_settings_upsert.sql_generated.go | 6 +- .../bulk_environment_upsert.sql_generated.go | 5 +- ...nd_by_project_id_and_slug.sql_generated.go | 5 +- ...onment_find_with_settings.sql_generated.go | 5 +- pkg/db/environment_insert.sql_generated.go | 26 ++++---- .../environment_list_preview.sql_generated.go | 5 +- ...t_runtime_settings_upsert.sql_generated.go | 10 ++- pkg/db/environment_upsert.sql_generated.go | 18 ++--- pkg/db/models_generated.go | 3 +- pkg/db/querier_generated.go | 18 ++--- pkg/db/queries/environment_insert.sql | 5 +- .../environment_runtime_settings_upsert.sql | 4 +- pkg/db/queries/environment_upsert.sql | 3 +- pkg/db/schema.sql | 3 +- svc/api/internal/testutil/seed/seed.go | 21 +++--- svc/ctrl/api/github_webhook.go | 2 +- svc/ctrl/integration/seed/seed.go | 21 +++--- .../services/deployment/create_deployment.go | 2 +- .../schema/environment_runtime_settings.ts | 2 +- web/internal/db/src/schema/environments.ts | 5 -- 22 files changed, 108 insertions(+), 132 deletions(-) diff --git a/cmd/dev/seed/local.go b/cmd/dev/seed/local.go index 966bc67e29..02dc530b06 100644 --- a/cmd/dev/seed/local.go +++ b/cmd/dev/seed/local.go @@ -131,23 +131,21 @@ func seedLocal(ctx context.Context, cmd *cli.Command) error { err = db.BulkQuery.InsertEnvironments(ctx, tx, []db.InsertEnvironmentParams{ { - ID: previewEnvID, - WorkspaceID: workspaceID, - ProjectID: projectID, - Slug: "preview", - Description: "", - CreatedAt: time.Now().UnixMilli(), - UpdatedAt: sql.NullInt64{Valid: false, Int64: 0}, - SentinelConfig: []byte{}, + ID: previewEnvID, + WorkspaceID: workspaceID, + ProjectID: projectID, + Slug: "preview", + Description: "", + CreatedAt: time.Now().UnixMilli(), + UpdatedAt: sql.NullInt64{Valid: false, Int64: 0}, }, { - ID: productionEnvID, - WorkspaceID: workspaceID, - ProjectID: projectID, - Slug: "production", - Description: "", - CreatedAt: time.Now().UnixMilli(), - UpdatedAt: sql.NullInt64{Valid: false, Int64: 0}, - SentinelConfig: []byte{}, + ID: productionEnvID, + WorkspaceID: workspaceID, + ProjectID: projectID, + Slug: "production", + Description: "", + CreatedAt: time.Now().UnixMilli(), + UpdatedAt: sql.NullInt64{Valid: false, Int64: 0}, }, }) if err != nil { @@ -157,29 +155,29 @@ func seedLocal(ctx context.Context, cmd *cli.Command) error { // Create default runtime settings for each environment err = db.BulkQuery.UpsertEnvironmentRuntimeSettings(ctx, tx, []db.UpsertEnvironmentRuntimeSettingsParams{ { - WorkspaceID: workspaceID, - EnvironmentID: previewEnvID, - Port: 8080, - CpuMillicores: 256, - MemoryMib: 256, - Command: dbtype.StringSlice{}, - Healthcheck: dbtype.NullHealthcheck{Healthcheck: nil, Valid: false}, - RegionConfig: dbtype.RegionConfig{}, - + WorkspaceID: workspaceID, + EnvironmentID: previewEnvID, + Port: 8080, + CpuMillicores: 256, + MemoryMib: 256, + Command: dbtype.StringSlice{}, + Healthcheck: dbtype.NullHealthcheck{Healthcheck: nil, Valid: false}, + RegionConfig: dbtype.RegionConfig{}, + SentinelConfig: []byte{}, ShutdownSignal: db.EnvironmentRuntimeSettingsShutdownSignalSIGTERM, CreatedAt: now, UpdatedAt: sql.NullInt64{Valid: true, Int64: now}, }, { - WorkspaceID: workspaceID, - EnvironmentID: productionEnvID, - Port: 8080, - CpuMillicores: 256, - MemoryMib: 256, - Command: dbtype.StringSlice{}, - Healthcheck: dbtype.NullHealthcheck{Healthcheck: nil, Valid: false}, - RegionConfig: dbtype.RegionConfig{}, - + WorkspaceID: workspaceID, + EnvironmentID: productionEnvID, + Port: 8080, + CpuMillicores: 256, + MemoryMib: 256, + Command: dbtype.StringSlice{}, + Healthcheck: dbtype.NullHealthcheck{Healthcheck: nil, Valid: false}, + RegionConfig: dbtype.RegionConfig{}, + SentinelConfig: []byte{}, ShutdownSignal: db.EnvironmentRuntimeSettingsShutdownSignalSIGTERM, CreatedAt: now, UpdatedAt: sql.NullInt64{Valid: true, Int64: now}, diff --git a/pkg/db/bulk_environment_insert.sql_generated.go b/pkg/db/bulk_environment_insert.sql_generated.go index 5b2688f8da..bf683304d8 100644 --- a/pkg/db/bulk_environment_insert.sql_generated.go +++ b/pkg/db/bulk_environment_insert.sql_generated.go @@ -9,7 +9,7 @@ import ( ) // bulkInsertEnvironment is the base query for bulk insert -const bulkInsertEnvironment = `INSERT INTO environments ( id, workspace_id, project_id, slug, description, created_at, updated_at, sentinel_config ) VALUES %s` +const bulkInsertEnvironment = `INSERT INTO environments ( id, workspace_id, project_id, slug, description, created_at, updated_at ) VALUES %s` // InsertEnvironments performs bulk insert in a single query func (q *BulkQueries) InsertEnvironments(ctx context.Context, db DBTX, args []InsertEnvironmentParams) error { @@ -21,7 +21,7 @@ func (q *BulkQueries) InsertEnvironments(ctx context.Context, db DBTX, args []In // Build the bulk insert query valueClauses := make([]string, len(args)) for i := range args { - valueClauses[i] = "( ?, ?, ?, ?, ?, ?, ?, ? )" + valueClauses[i] = "( ?, ?, ?, ?, ?, ?, ? )" } bulkQuery := fmt.Sprintf(bulkInsertEnvironment, strings.Join(valueClauses, ", ")) @@ -36,7 +36,6 @@ func (q *BulkQueries) InsertEnvironments(ctx context.Context, db DBTX, args []In allArgs = append(allArgs, arg.Description) allArgs = append(allArgs, arg.CreatedAt) allArgs = append(allArgs, arg.UpdatedAt) - allArgs = append(allArgs, arg.SentinelConfig) } // Execute the bulk insert diff --git a/pkg/db/bulk_environment_runtime_settings_upsert.sql_generated.go b/pkg/db/bulk_environment_runtime_settings_upsert.sql_generated.go index e8c7a8d802..d172145b17 100644 --- a/pkg/db/bulk_environment_runtime_settings_upsert.sql_generated.go +++ b/pkg/db/bulk_environment_runtime_settings_upsert.sql_generated.go @@ -9,7 +9,7 @@ import ( ) // bulkUpsertEnvironmentRuntimeSettings is the base query for bulk insert -const bulkUpsertEnvironmentRuntimeSettings = `INSERT INTO environment_runtime_settings ( workspace_id, environment_id, port, cpu_millicores, memory_mib, command, healthcheck, region_config, shutdown_signal, created_at, updated_at ) VALUES %s ON DUPLICATE KEY UPDATE +const bulkUpsertEnvironmentRuntimeSettings = `INSERT INTO environment_runtime_settings ( workspace_id, environment_id, port, cpu_millicores, memory_mib, command, healthcheck, region_config, shutdown_signal, sentinel_config, created_at, updated_at ) VALUES %s ON DUPLICATE KEY UPDATE port = VALUES(port), cpu_millicores = VALUES(cpu_millicores), memory_mib = VALUES(memory_mib), @@ -17,6 +17,7 @@ const bulkUpsertEnvironmentRuntimeSettings = `INSERT INTO environment_runtime_se healthcheck = VALUES(healthcheck), region_config = VALUES(region_config), shutdown_signal = VALUES(shutdown_signal), + sentinel_config = VALUES(sentinel_config), updated_at = VALUES(updated_at)` // UpsertEnvironmentRuntimeSettings performs bulk insert in a single query @@ -29,7 +30,7 @@ func (q *BulkQueries) UpsertEnvironmentRuntimeSettings(ctx context.Context, db D // Build the bulk insert query valueClauses := make([]string, len(args)) for i := range args { - valueClauses[i] = "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + valueClauses[i] = "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" } bulkQuery := fmt.Sprintf(bulkUpsertEnvironmentRuntimeSettings, strings.Join(valueClauses, ", ")) @@ -46,6 +47,7 @@ func (q *BulkQueries) UpsertEnvironmentRuntimeSettings(ctx context.Context, db D allArgs = append(allArgs, arg.Healthcheck) allArgs = append(allArgs, arg.RegionConfig) allArgs = append(allArgs, arg.ShutdownSignal) + allArgs = append(allArgs, arg.SentinelConfig) allArgs = append(allArgs, arg.CreatedAt) allArgs = append(allArgs, arg.UpdatedAt) } diff --git a/pkg/db/bulk_environment_upsert.sql_generated.go b/pkg/db/bulk_environment_upsert.sql_generated.go index 3438fdc668..13141aca14 100644 --- a/pkg/db/bulk_environment_upsert.sql_generated.go +++ b/pkg/db/bulk_environment_upsert.sql_generated.go @@ -9,7 +9,7 @@ import ( ) // bulkUpsertEnvironment is the base query for bulk insert -const bulkUpsertEnvironment = `INSERT INTO environments ( id, workspace_id, project_id, slug, sentinel_config, created_at ) VALUES %s ON DUPLICATE KEY UPDATE slug = VALUES(slug)` +const bulkUpsertEnvironment = `INSERT INTO environments ( id, workspace_id, project_id, slug, created_at ) VALUES %s ON DUPLICATE KEY UPDATE slug = VALUES(slug)` // UpsertEnvironment performs bulk insert in a single query func (q *BulkQueries) UpsertEnvironment(ctx context.Context, db DBTX, args []UpsertEnvironmentParams) error { @@ -21,7 +21,7 @@ func (q *BulkQueries) UpsertEnvironment(ctx context.Context, db DBTX, args []Ups // Build the bulk insert query valueClauses := make([]string, len(args)) for i := range args { - valueClauses[i] = "(?, ?, ?, ?, ?, ?)" + valueClauses[i] = "(?, ?, ?, ?, ?)" } bulkQuery := fmt.Sprintf(bulkUpsertEnvironment, strings.Join(valueClauses, ", ")) @@ -33,7 +33,6 @@ func (q *BulkQueries) UpsertEnvironment(ctx context.Context, db DBTX, args []Ups allArgs = append(allArgs, arg.WorkspaceID) allArgs = append(allArgs, arg.ProjectID) allArgs = append(allArgs, arg.Slug) - allArgs = append(allArgs, arg.SentinelConfig) allArgs = append(allArgs, arg.CreatedAt) } diff --git a/pkg/db/environment_find_by_project_id_and_slug.sql_generated.go b/pkg/db/environment_find_by_project_id_and_slug.sql_generated.go index aa38eb7817..71901325b8 100644 --- a/pkg/db/environment_find_by_project_id_and_slug.sql_generated.go +++ b/pkg/db/environment_find_by_project_id_and_slug.sql_generated.go @@ -10,7 +10,7 @@ import ( ) const findEnvironmentByProjectIdAndSlug = `-- name: FindEnvironmentByProjectIdAndSlug :one -SELECT pk, id, workspace_id, project_id, slug, description, sentinel_config, delete_protection, created_at, updated_at +SELECT pk, id, workspace_id, project_id, slug, description, delete_protection, created_at, updated_at FROM environments WHERE workspace_id = ? AND project_id = ? @@ -25,7 +25,7 @@ type FindEnvironmentByProjectIdAndSlugParams struct { // FindEnvironmentByProjectIdAndSlug // -// SELECT pk, id, workspace_id, project_id, slug, description, sentinel_config, delete_protection, created_at, updated_at +// SELECT pk, id, workspace_id, project_id, slug, description, delete_protection, created_at, updated_at // FROM environments // WHERE workspace_id = ? // AND project_id = ? @@ -40,7 +40,6 @@ func (q *Queries) FindEnvironmentByProjectIdAndSlug(ctx context.Context, db DBTX &i.ProjectID, &i.Slug, &i.Description, - &i.SentinelConfig, &i.DeleteProtection, &i.CreatedAt, &i.UpdatedAt, diff --git a/pkg/db/environment_find_with_settings.sql_generated.go b/pkg/db/environment_find_with_settings.sql_generated.go index 6487c9f57c..34ba22f453 100644 --- a/pkg/db/environment_find_with_settings.sql_generated.go +++ b/pkg/db/environment_find_with_settings.sql_generated.go @@ -11,7 +11,7 @@ import ( const findEnvironmentWithSettingsByProjectIdAndSlug = `-- name: FindEnvironmentWithSettingsByProjectIdAndSlug :one SELECT - e.pk, e.id, e.workspace_id, e.project_id, e.slug, e.description, e.sentinel_config, e.delete_protection, e.created_at, e.updated_at, + e.pk, e.id, e.workspace_id, e.project_id, e.slug, e.description, e.delete_protection, e.created_at, e.updated_at, ebs.pk, ebs.workspace_id, ebs.environment_id, ebs.dockerfile, ebs.docker_context, ebs.created_at, ebs.updated_at, ers.pk, ers.workspace_id, ers.environment_id, ers.port, ers.cpu_millicores, ers.memory_mib, ers.command, ers.healthcheck, ers.region_config, ers.shutdown_signal, ers.sentinel_config, ers.created_at, ers.updated_at FROM environments e @@ -37,7 +37,7 @@ type FindEnvironmentWithSettingsByProjectIdAndSlugRow struct { // FindEnvironmentWithSettingsByProjectIdAndSlug // // SELECT -// e.pk, e.id, e.workspace_id, e.project_id, e.slug, e.description, e.sentinel_config, e.delete_protection, e.created_at, e.updated_at, +// e.pk, e.id, e.workspace_id, e.project_id, e.slug, e.description, e.delete_protection, e.created_at, e.updated_at, // ebs.pk, ebs.workspace_id, ebs.environment_id, ebs.dockerfile, ebs.docker_context, ebs.created_at, ebs.updated_at, // ers.pk, ers.workspace_id, ers.environment_id, ers.port, ers.cpu_millicores, ers.memory_mib, ers.command, ers.healthcheck, ers.region_config, ers.shutdown_signal, ers.sentinel_config, ers.created_at, ers.updated_at // FROM environments e @@ -56,7 +56,6 @@ func (q *Queries) FindEnvironmentWithSettingsByProjectIdAndSlug(ctx context.Cont &i.Environment.ProjectID, &i.Environment.Slug, &i.Environment.Description, - &i.Environment.SentinelConfig, &i.Environment.DeleteProtection, &i.Environment.CreatedAt, &i.Environment.UpdatedAt, diff --git a/pkg/db/environment_insert.sql_generated.go b/pkg/db/environment_insert.sql_generated.go index fdecffdf2c..29cd1eb696 100644 --- a/pkg/db/environment_insert.sql_generated.go +++ b/pkg/db/environment_insert.sql_generated.go @@ -18,22 +18,20 @@ INSERT INTO environments ( slug, description, created_at, - updated_at, - sentinel_config + updated_at ) VALUES ( - ?, ?, ?, ?, ?, ?, ?, ? + ?, ?, ?, ?, ?, ?, ? ) ` type InsertEnvironmentParams struct { - ID string `db:"id"` - WorkspaceID string `db:"workspace_id"` - ProjectID string `db:"project_id"` - Slug string `db:"slug"` - Description string `db:"description"` - CreatedAt int64 `db:"created_at"` - UpdatedAt sql.NullInt64 `db:"updated_at"` - SentinelConfig []byte `db:"sentinel_config"` + ID string `db:"id"` + WorkspaceID string `db:"workspace_id"` + ProjectID string `db:"project_id"` + Slug string `db:"slug"` + Description string `db:"description"` + CreatedAt int64 `db:"created_at"` + UpdatedAt sql.NullInt64 `db:"updated_at"` } // InsertEnvironment @@ -45,10 +43,9 @@ type InsertEnvironmentParams struct { // slug, // description, // created_at, -// updated_at, -// sentinel_config +// updated_at // ) VALUES ( -// ?, ?, ?, ?, ?, ?, ?, ? +// ?, ?, ?, ?, ?, ?, ? // ) func (q *Queries) InsertEnvironment(ctx context.Context, db DBTX, arg InsertEnvironmentParams) error { _, err := db.ExecContext(ctx, insertEnvironment, @@ -59,7 +56,6 @@ func (q *Queries) InsertEnvironment(ctx context.Context, db DBTX, arg InsertEnvi arg.Description, arg.CreatedAt, arg.UpdatedAt, - arg.SentinelConfig, ) return err } diff --git a/pkg/db/environment_list_preview.sql_generated.go b/pkg/db/environment_list_preview.sql_generated.go index 980a524973..7ced31ad1d 100644 --- a/pkg/db/environment_list_preview.sql_generated.go +++ b/pkg/db/environment_list_preview.sql_generated.go @@ -10,7 +10,7 @@ import ( ) const listPreviewEnvironments = `-- name: ListPreviewEnvironments :many -SELECT pk, id, workspace_id, project_id, slug, description, sentinel_config, delete_protection, created_at, updated_at +SELECT pk, id, workspace_id, project_id, slug, description, delete_protection, created_at, updated_at FROM environments WHERE slug = 'preview' AND pk > ? @@ -25,7 +25,7 @@ type ListPreviewEnvironmentsParams struct { // ListPreviewEnvironments // -// SELECT pk, id, workspace_id, project_id, slug, description, sentinel_config, delete_protection, created_at, updated_at +// SELECT pk, id, workspace_id, project_id, slug, description, delete_protection, created_at, updated_at // FROM environments // WHERE slug = 'preview' // AND pk > ? @@ -47,7 +47,6 @@ func (q *Queries) ListPreviewEnvironments(ctx context.Context, db DBTX, arg List &i.ProjectID, &i.Slug, &i.Description, - &i.SentinelConfig, &i.DeleteProtection, &i.CreatedAt, &i.UpdatedAt, diff --git a/pkg/db/environment_runtime_settings_upsert.sql_generated.go b/pkg/db/environment_runtime_settings_upsert.sql_generated.go index 6bc361088a..aa07885a50 100644 --- a/pkg/db/environment_runtime_settings_upsert.sql_generated.go +++ b/pkg/db/environment_runtime_settings_upsert.sql_generated.go @@ -23,9 +23,10 @@ INSERT INTO environment_runtime_settings ( healthcheck, region_config, shutdown_signal, + sentinel_config, created_at, updated_at -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE port = VALUES(port), cpu_millicores = VALUES(cpu_millicores), @@ -34,6 +35,7 @@ ON DUPLICATE KEY UPDATE healthcheck = VALUES(healthcheck), region_config = VALUES(region_config), shutdown_signal = VALUES(shutdown_signal), + sentinel_config = VALUES(sentinel_config), updated_at = VALUES(updated_at) ` @@ -47,6 +49,7 @@ type UpsertEnvironmentRuntimeSettingsParams struct { Healthcheck dbtype.NullHealthcheck `db:"healthcheck"` RegionConfig dbtype.RegionConfig `db:"region_config"` ShutdownSignal EnvironmentRuntimeSettingsShutdownSignal `db:"shutdown_signal"` + SentinelConfig []byte `db:"sentinel_config"` CreatedAt int64 `db:"created_at"` UpdatedAt sql.NullInt64 `db:"updated_at"` } @@ -63,9 +66,10 @@ type UpsertEnvironmentRuntimeSettingsParams struct { // healthcheck, // region_config, // shutdown_signal, +// sentinel_config, // created_at, // updated_at -// ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +// ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) // ON DUPLICATE KEY UPDATE // port = VALUES(port), // cpu_millicores = VALUES(cpu_millicores), @@ -74,6 +78,7 @@ type UpsertEnvironmentRuntimeSettingsParams struct { // healthcheck = VALUES(healthcheck), // region_config = VALUES(region_config), // shutdown_signal = VALUES(shutdown_signal), +// sentinel_config = VALUES(sentinel_config), // updated_at = VALUES(updated_at) func (q *Queries) UpsertEnvironmentRuntimeSettings(ctx context.Context, db DBTX, arg UpsertEnvironmentRuntimeSettingsParams) error { _, err := db.ExecContext(ctx, upsertEnvironmentRuntimeSettings, @@ -86,6 +91,7 @@ func (q *Queries) UpsertEnvironmentRuntimeSettings(ctx context.Context, db DBTX, arg.Healthcheck, arg.RegionConfig, arg.ShutdownSignal, + arg.SentinelConfig, arg.CreatedAt, arg.UpdatedAt, ) diff --git a/pkg/db/environment_upsert.sql_generated.go b/pkg/db/environment_upsert.sql_generated.go index cdfdc025c5..caaabf2a25 100644 --- a/pkg/db/environment_upsert.sql_generated.go +++ b/pkg/db/environment_upsert.sql_generated.go @@ -15,19 +15,17 @@ INSERT INTO environments ( workspace_id, project_id, slug, - sentinel_config, created_at -) VALUES (?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE slug = VALUES(slug) ` type UpsertEnvironmentParams struct { - ID string `db:"id"` - WorkspaceID string `db:"workspace_id"` - ProjectID string `db:"project_id"` - Slug string `db:"slug"` - SentinelConfig []byte `db:"sentinel_config"` - CreatedAt int64 `db:"created_at"` + ID string `db:"id"` + WorkspaceID string `db:"workspace_id"` + ProjectID string `db:"project_id"` + Slug string `db:"slug"` + CreatedAt int64 `db:"created_at"` } // UpsertEnvironment @@ -37,9 +35,8 @@ type UpsertEnvironmentParams struct { // workspace_id, // project_id, // slug, -// sentinel_config, // created_at -// ) VALUES (?, ?, ?, ?, ?, ?) +// ) VALUES (?, ?, ?, ?, ?) // ON DUPLICATE KEY UPDATE slug = VALUES(slug) func (q *Queries) UpsertEnvironment(ctx context.Context, db DBTX, arg UpsertEnvironmentParams) error { _, err := db.ExecContext(ctx, upsertEnvironment, @@ -47,7 +44,6 @@ func (q *Queries) UpsertEnvironment(ctx context.Context, db DBTX, arg UpsertEnvi arg.WorkspaceID, arg.ProjectID, arg.Slug, - arg.SentinelConfig, arg.CreatedAt, ) return err diff --git a/pkg/db/models_generated.go b/pkg/db/models_generated.go index 4909b8369c..2cf3b186a1 100644 --- a/pkg/db/models_generated.go +++ b/pkg/db/models_generated.go @@ -1136,7 +1136,6 @@ type Environment struct { ProjectID string `db:"project_id"` Slug string `db:"slug"` Description string `db:"description"` - SentinelConfig []byte `db:"sentinel_config"` DeleteProtection sql.NullBool `db:"delete_protection"` CreatedAt int64 `db:"created_at"` UpdatedAt sql.NullInt64 `db:"updated_at"` @@ -1163,7 +1162,7 @@ type EnvironmentRuntimeSetting struct { Healthcheck dbtype.NullHealthcheck `db:"healthcheck"` RegionConfig dbtype.RegionConfig `db:"region_config"` ShutdownSignal EnvironmentRuntimeSettingsShutdownSignal `db:"shutdown_signal"` - SentinelConfig sql.NullString `db:"sentinel_config"` + SentinelConfig []byte `db:"sentinel_config"` CreatedAt int64 `db:"created_at"` UpdatedAt sql.NullInt64 `db:"updated_at"` } diff --git a/pkg/db/querier_generated.go b/pkg/db/querier_generated.go index 82d4c0b90a..2c4f70b650 100644 --- a/pkg/db/querier_generated.go +++ b/pkg/db/querier_generated.go @@ -293,7 +293,7 @@ type Querier interface { FindEnvironmentById(ctx context.Context, db DBTX, id string) (FindEnvironmentByIdRow, error) //FindEnvironmentByProjectIdAndSlug // - // SELECT pk, id, workspace_id, project_id, slug, description, sentinel_config, delete_protection, created_at, updated_at + // SELECT pk, id, workspace_id, project_id, slug, description, delete_protection, created_at, updated_at // FROM environments // WHERE workspace_id = ? // AND project_id = ? @@ -314,7 +314,7 @@ type Querier interface { //FindEnvironmentWithSettingsByProjectIdAndSlug // // SELECT - // e.pk, e.id, e.workspace_id, e.project_id, e.slug, e.description, e.sentinel_config, e.delete_protection, e.created_at, e.updated_at, + // e.pk, e.id, e.workspace_id, e.project_id, e.slug, e.description, e.delete_protection, e.created_at, e.updated_at, // ebs.pk, ebs.workspace_id, ebs.environment_id, ebs.dockerfile, ebs.docker_context, ebs.created_at, ebs.updated_at, // ers.pk, ers.workspace_id, ers.environment_id, ers.port, ers.cpu_millicores, ers.memory_mib, ers.command, ers.healthcheck, ers.region_config, ers.shutdown_signal, ers.sentinel_config, ers.created_at, ers.updated_at // FROM environments e @@ -1345,10 +1345,9 @@ type Querier interface { // slug, // description, // created_at, - // updated_at, - // sentinel_config + // updated_at // ) VALUES ( - // ?, ?, ?, ?, ?, ?, ?, ? + // ?, ?, ?, ?, ?, ?, ? // ) InsertEnvironment(ctx context.Context, db DBTX, arg InsertEnvironmentParams) error //InsertFrontlineRoute @@ -2109,7 +2108,7 @@ type Querier interface { ListPermissionsByRoleID(ctx context.Context, db DBTX, roleID string) ([]Permission, error) //ListPreviewEnvironments // - // SELECT pk, id, workspace_id, project_id, slug, description, sentinel_config, delete_protection, created_at, updated_at + // SELECT pk, id, workspace_id, project_id, slug, description, delete_protection, created_at, updated_at // FROM environments // WHERE slug = 'preview' // AND pk > ? @@ -2629,9 +2628,8 @@ type Querier interface { // workspace_id, // project_id, // slug, - // sentinel_config, // created_at - // ) VALUES (?, ?, ?, ?, ?, ?) + // ) VALUES (?, ?, ?, ?, ?) // ON DUPLICATE KEY UPDATE slug = VALUES(slug) UpsertEnvironment(ctx context.Context, db DBTX, arg UpsertEnvironmentParams) error //UpsertEnvironmentBuildSettings @@ -2661,9 +2659,10 @@ type Querier interface { // healthcheck, // region_config, // shutdown_signal, + // sentinel_config, // created_at, // updated_at - // ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + // ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) // ON DUPLICATE KEY UPDATE // port = VALUES(port), // cpu_millicores = VALUES(cpu_millicores), @@ -2672,6 +2671,7 @@ type Querier interface { // healthcheck = VALUES(healthcheck), // region_config = VALUES(region_config), // shutdown_signal = VALUES(shutdown_signal), + // sentinel_config = VALUES(sentinel_config), // updated_at = VALUES(updated_at) UpsertEnvironmentRuntimeSettings(ctx context.Context, db DBTX, arg UpsertEnvironmentRuntimeSettingsParams) error // Inserts a new identity or does nothing if one already exists for this workspace/external_id. diff --git a/pkg/db/queries/environment_insert.sql b/pkg/db/queries/environment_insert.sql index 39c4574fc5..7d2994ab84 100644 --- a/pkg/db/queries/environment_insert.sql +++ b/pkg/db/queries/environment_insert.sql @@ -6,8 +6,7 @@ INSERT INTO environments ( slug, description, created_at, - updated_at, - sentinel_config + updated_at ) VALUES ( - ?, ?, ?, ?, ?, ?, ?, ? + ?, ?, ?, ?, ?, ?, ? ); diff --git a/pkg/db/queries/environment_runtime_settings_upsert.sql b/pkg/db/queries/environment_runtime_settings_upsert.sql index d8a4a8fa52..4b368b48b5 100644 --- a/pkg/db/queries/environment_runtime_settings_upsert.sql +++ b/pkg/db/queries/environment_runtime_settings_upsert.sql @@ -9,9 +9,10 @@ INSERT INTO environment_runtime_settings ( healthcheck, region_config, shutdown_signal, + sentinel_config, created_at, updated_at -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE port = VALUES(port), cpu_millicores = VALUES(cpu_millicores), @@ -20,4 +21,5 @@ ON DUPLICATE KEY UPDATE healthcheck = VALUES(healthcheck), region_config = VALUES(region_config), shutdown_signal = VALUES(shutdown_signal), + sentinel_config = VALUES(sentinel_config), updated_at = VALUES(updated_at); diff --git a/pkg/db/queries/environment_upsert.sql b/pkg/db/queries/environment_upsert.sql index d7e501dcfa..d784d68ca4 100644 --- a/pkg/db/queries/environment_upsert.sql +++ b/pkg/db/queries/environment_upsert.sql @@ -4,7 +4,6 @@ INSERT INTO environments ( workspace_id, project_id, slug, - sentinel_config, created_at -) VALUES (?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE slug = VALUES(slug); diff --git a/pkg/db/schema.sql b/pkg/db/schema.sql index 9531913cb3..33a7b0ab5f 100644 --- a/pkg/db/schema.sql +++ b/pkg/db/schema.sql @@ -354,7 +354,6 @@ CREATE TABLE `environments` ( `project_id` varchar(256) NOT NULL, `slug` varchar(256) NOT NULL, `description` varchar(255) NOT NULL DEFAULT '', - `sentinel_config` longblob NOT NULL, `delete_protection` boolean DEFAULT false, `created_at` bigint NOT NULL, `updated_at` bigint, @@ -403,7 +402,7 @@ CREATE TABLE `environment_runtime_settings` ( `healthcheck` json, `region_config` json NOT NULL DEFAULT ('{}'), `shutdown_signal` enum('SIGTERM','SIGINT','SIGQUIT','SIGKILL') NOT NULL DEFAULT 'SIGTERM', - `sentinel_config` longblob, + `sentinel_config` longblob NOT NULL, `created_at` bigint NOT NULL, `updated_at` bigint, CONSTRAINT `environment_runtime_settings_pk` PRIMARY KEY(`pk`), diff --git a/svc/api/internal/testutil/seed/seed.go b/svc/api/internal/testutil/seed/seed.go index 13836c6317..77573a69e1 100644 --- a/svc/api/internal/testutil/seed/seed.go +++ b/svc/api/internal/testutil/seed/seed.go @@ -192,22 +192,17 @@ type CreateEnvironmentRequest struct { // CreateEnvironment creates an environment within a project. If SentinelConfig is // nil or empty, it defaults to "{}". func (s *Seeder) CreateEnvironment(ctx context.Context, req CreateEnvironmentRequest) db.Environment { - sentinelConfig := []byte("{}") - if len(req.SentinelConfig) > 0 { - sentinelConfig = req.SentinelConfig - } now := time.Now().UnixMilli() err := db.Query.InsertEnvironment(ctx, s.DB.RW(), db.InsertEnvironmentParams{ - ID: req.ID, - WorkspaceID: req.WorkspaceID, - ProjectID: req.ProjectID, - Slug: req.Slug, - Description: req.Description, - SentinelConfig: sentinelConfig, - CreatedAt: now, - UpdatedAt: sql.NullInt64{Int64: 0, Valid: false}, + ID: req.ID, + WorkspaceID: req.WorkspaceID, + ProjectID: req.ProjectID, + Slug: req.Slug, + Description: req.Description, + CreatedAt: now, + UpdatedAt: sql.NullInt64{Int64: 0, Valid: false}, }) require.NoError(s.t, err) @@ -220,6 +215,7 @@ func (s *Seeder) CreateEnvironment(ctx context.Context, req CreateEnvironmentReq Command: dbtype.StringSlice{}, Healthcheck: dbtype.NullHealthcheck{Healthcheck: nil, Valid: false}, RegionConfig: dbtype.RegionConfig{}, + SentinelConfig: []byte{}, ShutdownSignal: db.EnvironmentRuntimeSettingsShutdownSignalSIGTERM, CreatedAt: now, UpdatedAt: sql.NullInt64{Valid: true, Int64: now}, @@ -246,7 +242,6 @@ func (s *Seeder) CreateEnvironment(ctx context.Context, req CreateEnvironmentReq ProjectID: environment.ProjectID, Slug: environment.Slug, Description: req.Description, - SentinelConfig: sentinelConfig, DeleteProtection: sql.NullBool{Valid: true, Bool: req.DeleteProtection}, CreatedAt: now, UpdatedAt: sql.NullInt64{Int64: 0, Valid: false}, diff --git a/svc/ctrl/api/github_webhook.go b/svc/ctrl/api/github_webhook.go index ef90613f32..10f2efdd2d 100644 --- a/svc/ctrl/api/github_webhook.go +++ b/svc/ctrl/api/github_webhook.go @@ -194,7 +194,7 @@ func (s *GitHubWebhook) handlePush(ctx context.Context, w http.ResponseWriter, b WorkspaceID: project.WorkspaceID, ProjectID: project.ID, EnvironmentID: env.ID, - SentinelConfig: []byte(envSettings.EnvironmentRuntimeSetting.SentinelConfig.String), + SentinelConfig: envSettings.EnvironmentRuntimeSetting.SentinelConfig, EncryptedEnvironmentVariables: secretsBlob, Command: envSettings.EnvironmentRuntimeSetting.Command, Status: db.DeploymentsStatusPending, diff --git a/svc/ctrl/integration/seed/seed.go b/svc/ctrl/integration/seed/seed.go index a378104d4b..42f8754392 100644 --- a/svc/ctrl/integration/seed/seed.go +++ b/svc/ctrl/integration/seed/seed.go @@ -174,22 +174,17 @@ type CreateEnvironmentRequest struct { } func (s *Seeder) CreateEnvironment(ctx context.Context, req CreateEnvironmentRequest) db.Environment { - sentinelConfig := []byte("{}") - if len(req.SentinelConfig) > 0 { - sentinelConfig = req.SentinelConfig - } now := time.Now().UnixMilli() err := db.Query.InsertEnvironment(ctx, s.DB.RW(), db.InsertEnvironmentParams{ - ID: req.ID, - WorkspaceID: req.WorkspaceID, - ProjectID: req.ProjectID, - Slug: req.Slug, - Description: req.Description, - SentinelConfig: sentinelConfig, - CreatedAt: now, - UpdatedAt: sql.NullInt64{Int64: 0, Valid: false}, + ID: req.ID, + WorkspaceID: req.WorkspaceID, + ProjectID: req.ProjectID, + Slug: req.Slug, + Description: req.Description, + CreatedAt: now, + UpdatedAt: sql.NullInt64{Int64: 0, Valid: false}, }) require.NoError(s.t, err) @@ -202,6 +197,7 @@ func (s *Seeder) CreateEnvironment(ctx context.Context, req CreateEnvironmentReq Command: dbtype.StringSlice{}, Healthcheck: dbtype.NullHealthcheck{Healthcheck: nil, Valid: false}, RegionConfig: dbtype.RegionConfig{}, + SentinelConfig: []byte{}, ShutdownSignal: db.EnvironmentRuntimeSettingsShutdownSignalSIGTERM, CreatedAt: now, UpdatedAt: sql.NullInt64{Valid: true, Int64: now}, @@ -228,7 +224,6 @@ func (s *Seeder) CreateEnvironment(ctx context.Context, req CreateEnvironmentReq ProjectID: environment.ProjectID, Slug: environment.Slug, Description: req.Description, - SentinelConfig: sentinelConfig, DeleteProtection: sql.NullBool{Valid: true, Bool: req.DeleteProtection}, CreatedAt: now, UpdatedAt: sql.NullInt64{Int64: 0, Valid: false}, diff --git a/svc/ctrl/services/deployment/create_deployment.go b/svc/ctrl/services/deployment/create_deployment.go index 287cf5b321..aa4cdc185f 100644 --- a/svc/ctrl/services/deployment/create_deployment.go +++ b/svc/ctrl/services/deployment/create_deployment.go @@ -158,7 +158,7 @@ func (s *Service) CreateDeployment( ProjectID: req.Msg.GetProjectId(), EnvironmentID: env.ID, OpenapiSpec: sql.NullString{String: "", Valid: false}, - SentinelConfig: []byte(envSettings.EnvironmentRuntimeSetting.SentinelConfig.String), + SentinelConfig: envSettings.EnvironmentRuntimeSetting.SentinelConfig, EncryptedEnvironmentVariables: secretsBlob, Command: envSettings.EnvironmentRuntimeSetting.Command, Status: db.DeploymentsStatusPending, diff --git a/web/internal/db/src/schema/environment_runtime_settings.ts b/web/internal/db/src/schema/environment_runtime_settings.ts index 190c06fa9e..974d8f11f4 100644 --- a/web/internal/db/src/schema/environment_runtime_settings.ts +++ b/web/internal/db/src/schema/environment_runtime_settings.ts @@ -49,7 +49,7 @@ export const environmentRuntimeSettings = mysqlTable( .notNull() .default("SIGTERM"), - sentinelConfig: longblob("sentinel_config"), + sentinelConfig: longblob("sentinel_config").notNull(), ...lifecycleDates, }, diff --git a/web/internal/db/src/schema/environments.ts b/web/internal/db/src/schema/environments.ts index c942fe56c7..a01717a238 100644 --- a/web/internal/db/src/schema/environments.ts +++ b/web/internal/db/src/schema/environments.ts @@ -5,7 +5,6 @@ import { lifecycleDates } from "./util/lifecycle_dates"; import { workspaces } from "./workspaces"; import { projects } from "./projects"; -import { longblob } from "./util/longblob"; export const environments = mysqlTable( "environments", { @@ -18,10 +17,6 @@ export const environments = mysqlTable( slug: varchar("slug", { length: 256 }).notNull(), // URL-safe identifier within workspace description: varchar("description", { length: 255 }).notNull().default(""), - // @deprecated - // use environment_runtime_settings.sentinel_config instead - sentinelConfig: longblob("sentinel_config").notNull(), - ...deleteProtection, ...lifecycleDates, }, From 455e8d6e4bca1cff6490b9fc54dbe50226172fa0 Mon Sep 17 00:00:00 2001 From: chronark Date: Thu, 19 Feb 2026 18:14:36 +0100 Subject: [PATCH 8/8] fix: db --- web/apps/dashboard/lib/trpc/routers/deploy/project/create.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/apps/dashboard/lib/trpc/routers/deploy/project/create.ts b/web/apps/dashboard/lib/trpc/routers/deploy/project/create.ts index 141a3c7f0e..7bebb80bc0 100644 --- a/web/apps/dashboard/lib/trpc/routers/deploy/project/create.ts +++ b/web/apps/dashboard/lib/trpc/routers/deploy/project/create.ts @@ -93,7 +93,6 @@ export const createProject = workspaceProcedure projectId, slug: "production", description: "Production", - sentinelConfig: "", deleteProtection: false, createdAt: Date.now(), updatedAt: null, @@ -104,7 +103,6 @@ export const createProject = workspaceProcedure projectId, slug: "preview", description: "Preview", - sentinelConfig: "", deleteProtection: false, createdAt: Date.now(), updatedAt: null, @@ -124,10 +122,12 @@ export const createProject = workspaceProcedure { workspaceId: ctx.workspace.id, environmentId: prodEnvId, + sentinelConfig: "{}", }, { workspaceId: ctx.workspace.id, environmentId: previewEnvId, + sentinelConfig: "{}", }, ]); });