From 0bc43416640ebba124913e135386345fc6b9f165 Mon Sep 17 00:00:00 2001 From: Pascal van Leeuwen Date: Mon, 29 Sep 2025 18:31:58 +0100 Subject: [PATCH 1/4] Add new data source to get Portal Applications data from PostgREST --- env.example | 26 +++- env.go | 114 +++++++++++++++--- go.mod | 16 +++ go.sum | 34 ++++++ main.go | 38 ++++-- postgrest/driver.go | 259 ++++++++++++++++++++++++++++++++++++++++ postgrest/jwt.go | 52 ++++++++ postgrest/portal_app.go | 62 ++++++++++ store/data_source.go | 1 + 9 files changed, 577 insertions(+), 25 deletions(-) create mode 100644 postgrest/driver.go create mode 100644 postgrest/jwt.go create mode 100644 postgrest/portal_app.go diff --git a/env.example b/env.example index 2bbdd0d..a8cfd3c 100644 --- a/env.example +++ b/env.example @@ -2,10 +2,34 @@ # REQUIRED ENVIRONMENT VARIABLES # ================================ -# [REQUIRED]: PostgreSQL connection string for the PortalApp database used by the auth server. +# [OPTIONAL]: Data source type - "postgres" or "postgrest" +# - Default: "postgres" if not set +# DATA_SOURCE_TYPE="postgres" + +# [REQUIRED when DATA_SOURCE_TYPE=postgres]: PostgreSQL connection string for the PortalApp database used by the auth server. # - Example: "postgresql://username:password@localhost:5432/dbname" POSTGRES_CONNECTION_STRING= +# [REQUIRED when DATA_SOURCE_TYPE=postgrest]: PostgREST base URL +# - Example: "http://localhost:3000" +# POSTGREST_BASE_URL="http://localhost:3000" + +# [REQUIRED when DATA_SOURCE_TYPE=postgrest]: PostgREST JWT secret for authentication +# - Example: "supersecretjwtsecretforlocaldevelopment123456789" +# POSTGREST_JWT_SECRET="supersecretjwtsecretforlocaldevelopment123456789" + +# [OPTIONAL when DATA_SOURCE_TYPE=postgrest]: PostgREST JWT role +# POSTGREST_JWT_ROLE="authenticated" + +# [REQUIRED when DATA_SOURCE_TYPE=postgrest]: PostgREST JWT email for authentication +# - Example: "service@grove.city" +# POSTGREST_JWT_EMAIL="service@grove.city" + +# [OPTIONAL]: PostgREST request timeout +# - Default: 30s if not set +# - Examples: "30s", "1m", "2m30s" +# POSTGREST_TIMEOUT="30s" + # [REQUIRED]: GCP project ID for the data warehouse used by the rate limit store. # - Example: "your-project-id" GCP_PROJECT_ID= diff --git a/env.go b/env.go index 0cc0c60..f201f0f 100644 --- a/env.go +++ b/env.go @@ -13,14 +13,41 @@ import ( ) const ( + // [OPTIONAL]: Data source type - "postgres" or "postgrest" + // - Default: "postgres" if not set + dataSourceTypeEnv = "DATA_SOURCE_TYPE" + defaultDataSourceType = "postgres" + // [REQUIRED]: PostgreSQL connection string for the PortalApp database used by the auth server. // - Example: "postgresql://username:password@localhost:5432/dbname" postgresConnectionStringEnv = "POSTGRES_CONNECTION_STRING" + // [REQUIRED when DATA_SOURCE_TYPE=postgrest]: PostgREST base URL + // - Example: "http://localhost:3000" + postgrestBaseURLEnv = "POSTGREST_BASE_URL" + + // [REQUIRED when DATA_SOURCE_TYPE=postgrest]: JWT secret for PostgREST authentication + // - Example: "supersecretjwtsecretforlocaldevelopment123456789" + postgrestJWTSecretEnv = "POSTGREST_JWT_SECRET" + + // [OPTIONAL]: JWT role for PostgREST authentication + // - Examples: "authenticated", "anon" + postgrestJWTRoleEnv = "POSTGREST_JWT_ROLE" + + // [REQUIRED when DATA_SOURCE_TYPE=postgrest]: JWT email for PostgREST authentication + // - Example: "service@grove.city" + postgrestJWTEmailEnv = "POSTGREST_JWT_EMAIL" + // [REQUIRED]: GCP project ID for the data warehouse used by the rate limit store. // - Example: "your-project-id" gcpProjectIDEnv = "GCP_PROJECT_ID" + // [OPTIONAL]: PostgREST request timeout + // - Default: 30s if not set + // - Examples: "30s", "1m", "2m30s" + postgrestTimeoutEnv = "POSTGREST_TIMEOUT" + defaultPostgrestTimeout = 30 * time.Second + // [OPTIONAL]: Port to run the external auth server on. // - Default: 10001 if not set portEnv = "PORT" @@ -61,14 +88,31 @@ const ( var postgresConnectionStringRegex = regexp.MustCompile(`^postgres(?:ql)?:\/\/[^:]+:[^@]+@[^:]+:\d+\/[^?]+(?:\?.+)?$`) +// DataSourceType represents the type of data source to use +type DataSourceType string + +const ( + DataSourceTypePostgres DataSourceType = "postgres" + DataSourceTypePostgREST DataSourceType = "postgrest" +) + // envVars holds configuration values. // - All fields are private. // - Use gatherEnvVars to load, validate, and hydrate defaults from environment variables. type envVars struct { - // Database and external service configuration + // Database configuration + dataSourceType DataSourceType + postgresConnectionString string - gcpProjectID string + postgrestBaseURL string + postgrestJWTSecret string + postgrestJWTRole string + postgrestJWTEmail string + postgrestTimeout time.Duration + + // Data warehouse configuration + gcpProjectID string // Server port configuration port int metricsPort int @@ -87,10 +131,18 @@ type envVars struct { // - Loads configuration from environment variables // - Validates and hydrates defaults for missing/invalid values func gatherEnvVars() (envVars, error) { - // Initialize with Postgres connection string from environment + // Initialize with environment variables e := envVars{ + dataSourceType: DataSourceType(os.Getenv(dataSourceTypeEnv)), + postgresConnectionString: os.Getenv(postgresConnectionStringEnv), - gcpProjectID: os.Getenv(gcpProjectIDEnv), + + postgrestBaseURL: os.Getenv(postgrestBaseURLEnv), + postgrestJWTSecret: os.Getenv(postgrestJWTSecretEnv), + postgrestJWTRole: os.Getenv(postgrestJWTRoleEnv), + postgrestJWTEmail: os.Getenv(postgrestJWTEmailEnv), + + gcpProjectID: os.Getenv(gcpProjectIDEnv), } // Parse port environment variable (if provided) @@ -155,6 +207,16 @@ func gatherEnvVars() (envVars, error) { e.rateLimitStoreRefreshInterval = duration } + // Parse PostgREST timeout from environment (if provided) + postgrestTimeoutStr := os.Getenv(postgrestTimeoutEnv) + if postgrestTimeoutStr != "" { + duration, err := time.ParseDuration(postgrestTimeoutStr) + if err != nil { + return envVars{}, fmt.Errorf("invalid PostgREST timeout format: %v", err) + } + e.postgrestTimeout = duration + } + // Apply defaults for any unset configuration e.hydrateDefaults() @@ -167,23 +229,37 @@ func gatherEnvVars() (envVars, error) { // validate checks that all required environment variables are set and valid func (e *envVars) validate() error { - // Postgres connection string must be set - if e.postgresConnectionString == "" { - return fmt.Errorf("%s is not set", postgresConnectionStringEnv) - } - // GCP project ID must be set if e.gcpProjectID == "" { return fmt.Errorf("%s is not set", gcpProjectIDEnv) } - // Connection string must match expected format - matched, err := regexp.MatchString(postgresConnectionStringRegex.String(), e.postgresConnectionString) - if err != nil { - return fmt.Errorf("failed to validate postgresConnectionString: %v", err) - } - if !matched { - return fmt.Errorf("postgresConnectionString does not match the required pattern") + // Validate based on data source type + switch e.dataSourceType { + case DataSourceTypePostgres: + if e.postgresConnectionString == "" { + return fmt.Errorf("%s is required when DATA_SOURCE_TYPE=postgres", postgresConnectionStringEnv) + } + // Connection string must match expected format + matched, err := regexp.MatchString(postgresConnectionStringRegex.String(), e.postgresConnectionString) + if err != nil { + return fmt.Errorf("failed to validate postgresConnectionString: %v", err) + } + if !matched { + return fmt.Errorf("postgresConnectionString does not match the required pattern") + } + case DataSourceTypePostgREST: + if e.postgrestBaseURL == "" { + return fmt.Errorf("%s is required when DATA_SOURCE_TYPE=postgrest", postgrestBaseURLEnv) + } + if e.postgrestJWTSecret == "" { + return fmt.Errorf("%s is required when DATA_SOURCE_TYPE=postgrest", postgrestJWTSecretEnv) + } + if e.postgrestJWTEmail == "" { + return fmt.Errorf("%s is required when DATA_SOURCE_TYPE=postgrest", postgrestJWTEmailEnv) + } + default: + return fmt.Errorf("unsupported data source type: %s", e.dataSourceType) } return nil @@ -191,6 +267,12 @@ func (e *envVars) validate() error { // hydrateDefaults sets defaults for missing/invalid values func (e *envVars) hydrateDefaults() { + if e.dataSourceType == "" { + e.dataSourceType = DataSourceType(defaultDataSourceType) + } + if e.postgrestTimeout == 0 { + e.postgrestTimeout = defaultPostgrestTimeout + } if e.port == 0 { e.port = defaultPort } diff --git a/go.mod b/go.mod index d3bc7b5..8271b0a 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,13 @@ module github.com/buildwithgrove/path-external-auth-server go 1.24 +// TODO_IN_THIS_PR: Update to published version when Portal DB SDK is released +replace github.com/grove/path/portal-db/sdk/go => ../path/portal-db/sdk/go + require ( cloud.google.com/go/bigquery v1.69.0 github.com/envoyproxy/go-control-plane/envoy v1.32.4 + github.com/grove/path/portal-db/sdk/go v0.0.0-00010101000000-000000000000 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 github.com/ory/dockertest/v3 v3.12.0 @@ -28,6 +32,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -40,8 +45,11 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-viper/mapstructure/v2 v2.1.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -54,17 +62,24 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/moby/term v0.5.0 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oapi-codegen/runtime v1.1.1 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.2.3 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect @@ -74,6 +89,7 @@ require ( github.com/prometheus/procfs v0.16.1 // indirect github.com/rs/zerolog v1.32.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect diff --git a/go.sum b/go.sum index f218334..95532e0 100644 --- a/go.sum +++ b/go.sum @@ -36,10 +36,14 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -69,6 +73,8 @@ github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfU github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -76,8 +82,14 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= @@ -113,6 +125,9 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -123,8 +138,12 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -137,8 +156,16 @@ github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -147,6 +174,8 @@ github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19o github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -175,6 +204,7 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= @@ -182,6 +212,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/main.go b/main.go index d9e2609..e9b7aa2 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "github.com/buildwithgrove/path-external-auth-server/dwh" "github.com/buildwithgrove/path-external-auth-server/metrics" "github.com/buildwithgrove/path-external-auth-server/postgres/grove" + "github.com/buildwithgrove/path-external-auth-server/postgrest" "github.com/buildwithgrove/path-external-auth-server/ratelimit" "github.com/buildwithgrove/path-external-auth-server/store" ) @@ -38,15 +39,15 @@ func main() { // Create context for graceful shutdown ctx := context.Background() - // Create a new postgres data source - postgresDataSource, err := grove.NewGrovePostgresDriver( - logger, env.postgresConnectionString, - ) + // Create data source based on configuration + dataSource, err := createDataSource(env, logger) if err != nil { - panic(fmt.Sprintf("failed to connect to postgres: %v", err)) + panic(fmt.Sprintf("failed to create data source: %v", err)) } - defer postgresDataSource.Close() - logger.Info().Msg("🐘 Successfully connected to postgres as a data source") + defer dataSource.Close() + logger.Info(). + Str("data_source_type", string(env.dataSourceType)). + Msg("✅ Successfully connected to data source") // Create a new data warehouse driver dataWarehouseDriver, err := dwh.NewDriver(context.Background(), env.gcpProjectID) @@ -59,7 +60,7 @@ func main() { // Create a new portal app store portalAppStore, err := store.NewPortalAppStore( logger, - postgresDataSource, + dataSource, env.portalAppStoreRefreshInterval, ) if err != nil { @@ -135,3 +136,24 @@ func main() { panic(err) } } + +// createDataSource creates the appropriate data source based on the configured type +func createDataSource(env envVars, logger polylog.Logger) (store.DataSource, error) { + switch env.dataSourceType { + case DataSourceTypePostgres: + logger.Info().Msg("🐘 Creating Postgres data source") + return grove.NewGrovePostgresDriver(logger, env.postgresConnectionString) + case DataSourceTypePostgREST: + logger.Info().Msg("🌐 Creating PostgREST data source") + return postgrest.NewPostgRESTDriver( + logger, + env.postgrestBaseURL, + env.postgrestJWTSecret, + env.postgrestJWTRole, + env.postgrestJWTEmail, + env.postgrestTimeout, + ) + default: + return nil, fmt.Errorf("unsupported data source type: %s", env.dataSourceType) + } +} diff --git a/postgrest/driver.go b/postgrest/driver.go new file mode 100644 index 0000000..eec6c26 --- /dev/null +++ b/postgrest/driver.go @@ -0,0 +1,259 @@ +package postgrest + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/pokt-network/poktroll/pkg/polylog" + + "github.com/buildwithgrove/path-external-auth-server/store" + portal_db_sdk "github.com/grove/path/portal-db/sdk/go" +) + +// PostgRESTDriver implements the store.DataSource interface +// to provide data from PostgREST API for the portal app store. +var _ store.DataSource = &PostgRESTDriver{} + +type PostgRESTDriver struct { + logger polylog.Logger + + client *portal_db_sdk.ClientWithResponses + + // JWT secret for generating authentication tokens + jwtSecret string + jwtRole string + jwtEmail string + + timeout time.Duration +} + +// NewPostgRESTDriver creates a new PostgREST driver that implements the store.DataSource interface. +// +// The driver connects to a PostgREST API and: +// 1. Provides methods to fetch initial portal app data +// 2. Uses JWT authentication for API requests +// 3. Converts PostgREST responses to store.PortalApp format +func NewPostgRESTDriver( + logger polylog.Logger, + baseURL string, + jwtSecret string, + jwtRole string, + jwtEmail string, + timeout time.Duration, +) (*PostgRESTDriver, error) { + // Create PostgREST client with custom HTTP client + client, err := portal_db_sdk.NewClientWithResponses(baseURL) + if err != nil { + return nil, fmt.Errorf("failed to create PostgREST client: %w", err) + } + + driver := &PostgRESTDriver{ + logger: logger, + client: client, + jwtSecret: jwtSecret, + jwtRole: jwtRole, + jwtEmail: jwtEmail, + timeout: timeout, + } + + return driver, nil +} + +// getRequestEditor generates a fresh JWT token and returns a request editor function +func (d *PostgRESTDriver) getRequestEditor(token string) (portal_db_sdk.RequestEditorFn, error) { + // Return request editor with the generated token + return func(ctx context.Context, req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/json") + return nil + }, nil +} + +// GetPortalApps loads the full set of PortalApps from the PostgREST API. +// It uses a two-step approach: +// 1. Fetch portal applications +// 2. Fetch portal accounts to get plan types +// 3. Merge the data and convert to store.PortalApp format +func (d *PostgRESTDriver) GetPortalApps() (map[store.PortalAppID]*store.PortalApp, error) { + d.logger.Info().Msg("🌐 Executing PostgREST portal apps query...") + + // Step 1: Get portal applications + applications, err := d.getPortalApplications() + if err != nil { + return nil, fmt.Errorf("failed to fetch portal applications: %w", err) + } + + if len(applications) == 0 { + d.logger.Info().Msg("✅ No portal applications found") + return make(map[store.PortalAppID]*store.PortalApp), nil + } + + // Step 2: Get all portal accounts (we'll match them in Go code) + accounts, err := d.getAllPortalAccounts() + if err != nil { + return nil, fmt.Errorf("failed to fetch portal accounts: %w", err) + } + + d.logger.Info(). + Int("num_applications", len(applications)). + Int("num_accounts", len(accounts)). + Msg("✅ Successfully fetched Portal Applications and Accounts from PostgREST") + + // Step 3: Convert and merge data + return d.convertToPortalApps(applications, accounts), nil +} + +// getPortalApplications fetches portal applications from PostgREST API +func (d *PostgRESTDriver) getPortalApplications() ([]portal_db_sdk.PortalApplications, error) { + ctx, cancel := context.WithTimeout(context.Background(), d.timeout) + defer cancel() + + // Build select fields for readability + selectFields := strings.Join([]string{ + "portal_application_id", + "portal_account_id", + "secret_key_hash", + "secret_key_required", + "portal_application_user_limit", + }, ",") + + params := &portal_db_sdk.GetPortalApplicationsParams{ + Select: stringPtr(selectFields), + DeletedAt: stringPtr("is.null"), // Only active applications + } + + // Generate a fresh JWT token + token, err := generatePostgRESTJWT(d.jwtSecret, d.jwtRole, d.jwtEmail) + if err != nil { + return nil, fmt.Errorf("failed to generate JWT token: %w", err) + } + + requestEditor, err := d.getRequestEditor(token) + if err != nil { + return nil, fmt.Errorf("failed to create request editor: %w", err) + } + + resp, err := d.client.GetPortalApplicationsWithResponse(ctx, params, requestEditor) + if err != nil { + d.logger.Error().Err(err).Msg("failed to call PostgREST portal applications endpoint") + return nil, fmt.Errorf("failed to call PostgREST API: %w", err) + } + + if resp.StatusCode() != 200 { + d.logger.Error().Int("status_code", resp.StatusCode()).Msg("unexpected status code from PostgREST") + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) + } + + // Parse JSON response directly + var applications []portal_db_sdk.PortalApplications + if err := json.Unmarshal(resp.Body, &applications); err != nil { + d.logger.Error().Err(err).Msg("failed to parse portal applications response") + return nil, fmt.Errorf("failed to parse JSON response: %w", err) + } + + return applications, nil +} + +// getAllPortalAccounts fetches all non-deleted portal accounts with plan types from PostgREST API +func (d *PostgRESTDriver) getAllPortalAccounts() ([]portal_db_sdk.PortalAccounts, error) { + ctx, cancel := context.WithTimeout(context.Background(), d.timeout) + defer cancel() + + // Build select fields for minimal data transfer + selectFields := strings.Join([]string{ + "portal_account_id", + "portal_plan_type", + "portal_account_user_limit", + }, ",") + + params := &portal_db_sdk.GetPortalAccountsParams{ + Select: stringPtr(selectFields), + DeletedAt: stringPtr("is.null"), // Only active accounts + } + + // Generate a fresh JWT token + token, err := generatePostgRESTJWT(d.jwtSecret, d.jwtRole, d.jwtEmail) + if err != nil { + return nil, fmt.Errorf("failed to generate JWT token: %w", err) + } + + requestEditor, err := d.getRequestEditor(token) + if err != nil { + return nil, fmt.Errorf("failed to create request editor: %w", err) + } + + resp, err := d.client.GetPortalAccountsWithResponse(ctx, params, requestEditor) + if err != nil { + d.logger.Error().Err(err).Msg("failed to call PostgREST portal accounts endpoint") + return nil, fmt.Errorf("failed to call PostgREST API: %w", err) + } + + if resp.StatusCode() != 200 { + d.logger.Error().Int("status_code", resp.StatusCode()).Msg("unexpected status code from PostgREST") + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) + } + + // Parse JSON response directly + var accounts []portal_db_sdk.PortalAccounts + if err := json.Unmarshal(resp.Body, &accounts); err != nil { + d.logger.Error().Err(err).Msg("failed to parse portal accounts response") + return nil, fmt.Errorf("failed to parse JSON response: %w", err) + } + + return accounts, nil +} + +// buildAccountPlanMap creates a map of account ID to plan type for efficient lookups +func (d *PostgRESTDriver) buildAccountMap(accounts []portal_db_sdk.PortalAccounts) map[string]portal_db_sdk.PortalAccounts { + accountsMap := make(map[string]portal_db_sdk.PortalAccounts, len(accounts)) + for _, account := range accounts { + accountsMap[account.PortalAccountId] = account + } + return accountsMap +} + +// convertToPortalApps converts PostgREST responses to store.PortalApp format +func (d *PostgRESTDriver) convertToPortalApps( + applications []portal_db_sdk.PortalApplications, + accounts []portal_db_sdk.PortalAccounts, +) map[store.PortalAppID]*store.PortalApp { + // Build efficient lookup map for account plan types + accountsMap := d.buildAccountMap(accounts) + + // Convert applications to PortalApps + portalApps := make(map[store.PortalAppID]*store.PortalApp, len(applications)) + + for _, app := range applications { + // Get plan type from account mapping + account, exists := accountsMap[app.PortalAccountId] + if !exists { + d.logger.Warn(). + Str("portal_application_id", app.PortalApplicationId). + Str("portal_account_id", app.PortalAccountId). + Msg("⁉️ SHOULD NEVER HAPPEN - No plan type found for account, skipping application") + continue + } + + // Convert SDK types directly to store.PortalApp + portalApp := convertSDKToPortalApp(app, account) + + portalApps[store.PortalAppID(app.PortalApplicationId)] = portalApp + } + + return portalApps +} + +// Close is a no-op for the PostgREST driver. +// TODO_TECHDEBT(@commoddity): Remove this method when direct Postgres data source is removed. +func (d *PostgRESTDriver) Close() { + // Do nothing; only here to satisfy the DataSource interface +} + +// stringPtr is a helper function to create string pointers +func stringPtr(s string) *string { + return &s +} diff --git a/postgrest/jwt.go b/postgrest/jwt.go new file mode 100644 index 0000000..57c8a33 --- /dev/null +++ b/postgrest/jwt.go @@ -0,0 +1,52 @@ +package postgrest + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "time" +) + +// Use const header since it never changes for PostgREST +const ( + header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" // {"alg":"HS256","typ":"JWT"} + expiration = 1 * time.Hour +) + +// generatePostgRESTJWT creates a JWT token for PostgREST authentication using Go best practices. +// Uses stdlib encoding/base64.RawURLEncoding which handles URL-safe base64 without padding. +func generatePostgRESTJWT(secret, role, email string) (string, error) { + now := time.Now() + + // Create payload with minimal required claims for PostgREST + payload := map[string]interface{}{ + "role": role, + "email": email, + "exp": now.Add(expiration).Unix(), + } + + // Marshal payload to JSON + payloadJSON, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("failed to marshal JWT payload: %w", err) + } + + // Encode payload using stdlib RawURLEncoding (no padding, URL-safe) + encodedPayload := base64.RawURLEncoding.EncodeToString(payloadJSON) + + // Create unsigned token + unsignedToken := header + "." + encodedPayload + + // Sign with HMAC-SHA256 + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(unsignedToken)) + signature := h.Sum(nil) + + // Encode signature using stdlib RawURLEncoding + encodedSignature := base64.RawURLEncoding.EncodeToString(signature) + + // Return complete JWT + return unsignedToken + "." + encodedSignature, nil +} diff --git a/postgrest/portal_app.go b/postgrest/portal_app.go new file mode 100644 index 0000000..1ee9cf2 --- /dev/null +++ b/postgrest/portal_app.go @@ -0,0 +1,62 @@ +package postgrest + +import ( + "github.com/buildwithgrove/path-external-auth-server/store" + portal_db_sdk "github.com/grove/path/portal-db/sdk/go" +) + +const ( + // Plan types from the database - matching Grove's implementation + PlanFree_DatabaseType store.PlanType = "PLAN_FREE" + PlanUnlimited_DatabaseType store.PlanType = "PLAN_UNLIMITED" +) + +// convertSDKToPortalApp converts SDK types directly to store.PortalApp +// combining portal_db_sdk.PortalApplications and portal_db_sdk.PortalAccounts data. +func convertSDKToPortalApp( + app portal_db_sdk.PortalApplications, + account portal_db_sdk.PortalAccounts, +) *store.PortalApp { + planType := store.PlanType(account.PortalPlanType) + return &store.PortalApp{ + ID: store.PortalAppID(app.PortalApplicationId), + AccountID: store.AccountID(app.PortalAccountId), + PlanType: planType, + Auth: getAuthDetailsFromSDK(app), + RateLimit: getRateLimitDetailsFromSDK(account), + } +} + +// getAuthDetailsFromSDK determines the authentication configuration from SDK data +func getAuthDetailsFromSDK(app portal_db_sdk.PortalApplications) *store.Auth { + if app.SecretKeyRequired != nil && *app.SecretKeyRequired && app.SecretKeyHash != nil { + return &store.Auth{ + APIKey: *app.SecretKeyHash, + } + } + + return nil +} + +// getRateLimitDetailsFromSDK determines the rate limiting configuration from SDK data +func getRateLimitDetailsFromSDK(account portal_db_sdk.PortalAccounts) *store.RateLimit { + // The following scenarios are rate limited: + // - PLAN_FREE + // - PLAN_UNLIMITED with a user-specified monthly user limits + planType := store.PlanType(account.PortalPlanType) + + // Free plans are always rate limited so we set the limit a non-nil RateLimit field. + if planType == PlanFree_DatabaseType { + return &store.RateLimit{} + } + + // Unlimited plans with optional user-specified limits are rate limited. + if planType == PlanUnlimited_DatabaseType && account.PortalAccountUserLimit != nil && *account.PortalAccountUserLimit > 0 { + return &store.RateLimit{ + MonthlyUserLimit: int32(*account.PortalAccountUserLimit), + } + } + + // All other scenarios are not rate limited + return nil +} diff --git a/store/data_source.go b/store/data_source.go index 4f3caec..feb6475 100644 --- a/store/data_source.go +++ b/store/data_source.go @@ -10,6 +10,7 @@ type DataSource interface { // FetchInitialData loads the initial set of portal apps. GetPortalApps() (map[PortalAppID]*PortalApp, error) + // TODO_TECHDEBT(@commoddity): Remove this method when direct Postgres data source is removed. // Close closes the data source and cleans up any resources. Close() } From 5031e5ba70c465bef49ef8470c4aa3ac13ce0e38 Mon Sep 17 00:00:00 2001 From: Pascal van Leeuwen Date: Mon, 29 Sep 2025 18:38:10 +0100 Subject: [PATCH 2/4] update README.md --- README.md | 81 +++++++++++++++++++++++++++++++------- env.example | 14 +++---- env.go | 6 +-- grafana/local/load_test.sh | 2 +- 4 files changed, 77 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 315fc17..41278f2 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ - [Portal App Store Refresh](#portal-app-store-refresh) - [How does Portal App Store Refresh Work?](#how-does-portal-app-store-refresh-work) - [Configuration](#configuration) +- [PostgREST Data Source](#postgrest-data-source) + - [PostgREST Configuration](#postgrest-configuration) + - [JWT Authentication](#jwt-authentication) - [Envoy Gateway Integration](#envoy-gateway-integration) - [Prometheus Metrics](#prometheus-metrics) - [Key Metrics](#key-metrics) @@ -120,8 +123,8 @@ PEAS adds the following headers to authorized requests before forwarding them to | Header | Contents | Included For All Requests | Example Value | | ----------------------- | ---------------------------------------------- | ------------------------- | ------------- | -| `Portal-Application-ID` | The portal app ID of the authorized portal app | ✅ | "a12b3c4d" | -| `Portal-Account-ID` | The account ID associated with the portal app | ✅ | "3f4g2js2" | +| `Portal-Application-ID` | The portal app ID of the authorized portal app | ✅ | "a12b3c4d" | +| `Portal-Account-ID` | The account ID associated with the portal app | ✅ | "3f4g2js2" | ## Rate Limiting Implementation @@ -164,6 +167,44 @@ The refresh interval is configurable via the `REFRESH_INTERVAL` environment vari - **Format**: Duration string (e.g., `30s`, `1m`, `2m30s`) - **Purpose**: Balance between data freshness and database load +## PostgREST Data Source + +PEAS supports [PostgREST](https://docs.postgrest.org/en/v13/) as an alternative data source to direct PostgreSQL connections. PostgREST provides a RESTful API layer over PostgreSQL databases, enabling PEAS to fetch portal application and account data via HTTP rather than direct database connections. + +For more information about PostgREST configuration and usage, see the [official PostgREST documentation](https://docs.postgrest.org/en/v13/). + +
+ + PostgREST logo + +
+ +### PostgREST Configuration + +To use PostgREST as the data source, configure the following environment variables: + +```bash +# Set data source type to PostgREST +DATA_SOURCE_TYPE="postgrest" + +# PostgREST API endpoint +POSTGREST_BASE_URL="https://db.rpc.com/api" + +# JWT authentication (required for PostgREST access) +POSTGREST_JWT_SECRET="supersecretjwtsecretforlocaldevelopment123456789" +POSTGREST_JWT_EMAIL="service@rpc.com" +POSTGREST_JWT_ROLE="admin" + +# Request timeout (optional) +POSTGREST_TIMEOUT="30s" # Optional, defaults to 30s +``` + +### JWT Authentication + +PEAS generates fresh JWT tokens for each PostgREST API request using the configured secret, email, and role. + +This ensures secure access to the PostgREST API with proper authentication and authorization based on your [PostgREST JWT configuration](https://docs.postgrest.org/en/v13/references/auth.html). + ## Envoy Gateway Integration PEAS exposes a gRPC service that adheres to the spec expected by Envoy Proxy's `ext_authz` HTTP Filter. @@ -285,17 +326,27 @@ This tool uses gRPC reflection to communicate with PEAS, testing the same author PEAS is configured via environment variables. -| Variable | Required | Type | Description | Example | Default Value | -| --------------------------------- | -------- | -------- | ------------------------------------------------------------ | ---------------------------------------------------- | ------------- | -| POSTGRES_CONNECTION_STRING | ✅ | string | PostgreSQL connection string for the PortalApp database | postgresql://username:password@localhost:5432/dbname | - | -| GCP_PROJECT_ID | ✅ | string | GCP project ID for the data warehouse used by rate limiting | your-project-id | - | -| PORT | ❌ | int | Port to run the external auth server on | 10001 | 10001 | -| METRICS_PORT | ❌ | int | Port to run the Prometheus metrics server on | 9090 | 9090 | -| PPROF_PORT | ❌ | int | Port to run the pprof server on | 6060 | 6060 | -| LOGGER_LEVEL | ❌ | string | Log level for the external auth server | info, debug, warn, error | info | -| IMAGE_TAG | ❌ | string | Image tag/version for the application | v1.0.0 | development | -| PORTAL_APP_STORE_REFRESH_INTERVAL | ❌ | duration | Refresh interval for portal app data from the database | 30s, 1m, 2m30s | 30s | -| RATE_LIMIT_STORE_REFRESH_INTERVAL | ❌ | duration | Refresh interval for rate limit data from the data warehouse | 30s, 1m, 2m30s | 5m | +| Variable | Required | Type | Description | Example | Default Value | +| --------------------------------- | -------- | -------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------- | +| **Data Source Configuration** | | | | | | +| DATA_SOURCE_TYPE | ❌ | string | Data source type: "postgres" or "postgrest" | postgres, postgrest | postgres | +| POSTGRES_CONNECTION_STRING | ✅* | string | PostgreSQL connection string (required when DATA_SOURCE_TYPE=postgres) | postgresql://username:password@localhost:5432/dbname | - | +| POSTGREST_BASE_URL | ✅* | string | PostgREST API base URL (required when DATA_SOURCE_TYPE=postgrest) | https://db.rpc.com/api | - | +| POSTGREST_JWT_SECRET | ✅* | string | JWT secret for PostgREST authentication (required when DATA_SOURCE_TYPE=postgrest) | supersecretjwtsecretforlocaldevelopment123456789 | - | +| POSTGREST_JWT_EMAIL | ✅* | string | JWT email for PostgREST authentication (required when DATA_SOURCE_TYPE=postgrest) | service@rpc.com | - | +| POSTGREST_JWT_ROLE | ❌ | string | JWT role for PostgREST authentication | admin | - | +| POSTGREST_TIMEOUT | ❌ | duration | PostgREST request timeout | 30s, 1m, 2m30s | 30s | +| **System Configuration** | | | | | | +| GCP_PROJECT_ID | ✅ | string | GCP project ID for the data warehouse used by rate limiting | your-project-id | - | +| PORT | ❌ | int | Port to run the external auth server on | 10001 | 10001 | +| METRICS_PORT | ❌ | int | Port to run the Prometheus metrics server on | 9090 | 9090 | +| PPROF_PORT | ❌ | int | Port to run the pprof server on | 6060 | 6060 | +| LOGGER_LEVEL | ❌ | string | Log level for the external auth server | info, debug, warn, error | info | +| IMAGE_TAG | ❌ | string | Image tag/version for the application | v1.0.0 | development | +| PORTAL_APP_STORE_REFRESH_INTERVAL | ❌ | duration | Refresh interval for portal app data from the database | 30s, 1m, 2m30s | 30s | +| RATE_LIMIT_STORE_REFRESH_INTERVAL | ❌ | duration | Refresh interval for rate limit data from the data warehouse | 30s, 1m, 2m30s | 5m | + +**\* Required when the corresponding DATA_SOURCE_TYPE is selected** ## Developing Metrics Dashboard Locally @@ -329,9 +380,9 @@ This section describes how to run and test the PEAS metrics dashboard locally us - **PEAS Health**: `http://localhost:9090/healthz` - **PEAS pprof**: `http://localhost:6060/debug/pprof/` - **Prometheus**: `http://localhost:9091` - - **Grafana**: `http://localhost:3000` (admin/admin) + - **Grafana**: `https://db.rpc.com/api` (admin/admin) 4. **View the dashboard**: - - Go to Grafana at `http://localhost:3000` + - Go to Grafana at `https://db.rpc.com/api` - Login with admin/admin - The PEAS dashboard should be automatically loaded diff --git a/env.example b/env.example index a8cfd3c..0bba39d 100644 --- a/env.example +++ b/env.example @@ -11,19 +11,19 @@ POSTGRES_CONNECTION_STRING= # [REQUIRED when DATA_SOURCE_TYPE=postgrest]: PostgREST base URL -# - Example: "http://localhost:3000" -# POSTGREST_BASE_URL="http://localhost:3000" +# - Example: "https://db.rpc.com/api" +# POSTGREST_BASE_URL="https://db.rpc.com/api" # [REQUIRED when DATA_SOURCE_TYPE=postgrest]: PostgREST JWT secret for authentication # - Example: "supersecretjwtsecretforlocaldevelopment123456789" # POSTGREST_JWT_SECRET="supersecretjwtsecretforlocaldevelopment123456789" -# [OPTIONAL when DATA_SOURCE_TYPE=postgrest]: PostgREST JWT role -# POSTGREST_JWT_ROLE="authenticated" +# [REQUIRED when DATA_SOURCE_TYPE=postgrest]: PostgREST JWT role +# POSTGREST_JWT_ROLE="admin" # [REQUIRED when DATA_SOURCE_TYPE=postgrest]: PostgREST JWT email for authentication -# - Example: "service@grove.city" -# POSTGREST_JWT_EMAIL="service@grove.city" +# - Example: "service@rpc.com" +# POSTGREST_JWT_EMAIL="service@rpc.com" # [OPTIONAL]: PostgREST request timeout # - Default: 30s if not set @@ -32,7 +32,7 @@ POSTGRES_CONNECTION_STRING= # [REQUIRED]: GCP project ID for the data warehouse used by the rate limit store. # - Example: "your-project-id" -GCP_PROJECT_ID= +GCP_PROJECT_ID="your-project-id" # ================================ # OPTIONAL ENVIRONMENT VARIABLES diff --git a/env.go b/env.go index f201f0f..47946c1 100644 --- a/env.go +++ b/env.go @@ -23,7 +23,7 @@ const ( postgresConnectionStringEnv = "POSTGRES_CONNECTION_STRING" // [REQUIRED when DATA_SOURCE_TYPE=postgrest]: PostgREST base URL - // - Example: "http://localhost:3000" + // - Example: "https://db.rpc.com/api" postgrestBaseURLEnv = "POSTGREST_BASE_URL" // [REQUIRED when DATA_SOURCE_TYPE=postgrest]: JWT secret for PostgREST authentication @@ -31,11 +31,11 @@ const ( postgrestJWTSecretEnv = "POSTGREST_JWT_SECRET" // [OPTIONAL]: JWT role for PostgREST authentication - // - Examples: "authenticated", "anon" + // - Examples: "admin" postgrestJWTRoleEnv = "POSTGREST_JWT_ROLE" // [REQUIRED when DATA_SOURCE_TYPE=postgrest]: JWT email for PostgREST authentication - // - Example: "service@grove.city" + // - Example: "service@rpc.com" postgrestJWTEmailEnv = "POSTGREST_JWT_EMAIL" // [REQUIRED]: GCP project ID for the data warehouse used by the rate limit store. diff --git a/grafana/local/load_test.sh b/grafana/local/load_test.sh index 6338d81..504909f 100755 --- a/grafana/local/load_test.sh +++ b/grafana/local/load_test.sh @@ -164,5 +164,5 @@ echo "✅ Successful: $successful_count" echo "❌ Failed auth: $failed_count" echo "⚠️ Errors: $error_count" echo -echo -e "${YELLOW}📊 Check your Grafana dashboard at http://localhost:3000${NC}" +echo -e "${YELLOW}📊 Check your Grafana dashboard at https://db.rpc.com/api${NC}" echo -e "${YELLOW}📈 Check metrics at http://localhost:9090/metrics${NC}" From 1eeba389b17e2635a200394c28af537fe4c953fa Mon Sep 17 00:00:00 2001 From: Pascal van Leeuwen Date: Mon, 6 Oct 2025 18:09:44 +0100 Subject: [PATCH 3/4] remove replace in go.mod --- go.mod | 6 ++---- go.sum | 2 ++ postgrest/driver.go | 2 +- postgrest/portal_app.go | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8271b0a..d90ff45 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,11 @@ module github.com/buildwithgrove/path-external-auth-server go 1.24 -// TODO_IN_THIS_PR: Update to published version when Portal DB SDK is released -replace github.com/grove/path/portal-db/sdk/go => ../path/portal-db/sdk/go - require ( cloud.google.com/go/bigquery v1.69.0 + // TODO_NEXT(@commoddity): Update to concrete version tag when PATH PostgREST client SDK is released + github.com/buildwithgrove/path/portal-db/sdk/go v0.0.0-20251006170518-7fd85f003655 github.com/envoyproxy/go-control-plane/envoy v1.32.4 - github.com/grove/path/portal-db/sdk/go v0.0.0-00010101000000-000000000000 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 github.com/ory/dockertest/v3 v3.12.0 diff --git a/go.sum b/go.sum index 95532e0..fb8890f 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/buildwithgrove/path/portal-db/sdk/go v0.0.0-20251006170518-7fd85f003655 h1:ftU/Aj7YLzDJGZJtxonlpvUuBOGD5uLWViYh6YvCo8A= +github.com/buildwithgrove/path/portal-db/sdk/go v0.0.0-20251006170518-7fd85f003655/go.mod h1:Mqzhv+MOs0YBm34p/Yf/eW+MEpMnyxB1hNtYZNC+fHc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/postgrest/driver.go b/postgrest/driver.go index eec6c26..f7ad788 100644 --- a/postgrest/driver.go +++ b/postgrest/driver.go @@ -11,7 +11,7 @@ import ( "github.com/pokt-network/poktroll/pkg/polylog" "github.com/buildwithgrove/path-external-auth-server/store" - portal_db_sdk "github.com/grove/path/portal-db/sdk/go" + portal_db_sdk "github.com/buildwithgrove/path/portal-db/sdk/go" ) // PostgRESTDriver implements the store.DataSource interface diff --git a/postgrest/portal_app.go b/postgrest/portal_app.go index 1ee9cf2..69de957 100644 --- a/postgrest/portal_app.go +++ b/postgrest/portal_app.go @@ -1,8 +1,9 @@ package postgrest import ( + portal_db_sdk "github.com/buildwithgrove/path/portal-db/sdk/go" + "github.com/buildwithgrove/path-external-auth-server/store" - portal_db_sdk "github.com/grove/path/portal-db/sdk/go" ) const ( From 4948c1dc9254adf4d37b6bef7126aa73db99e36b Mon Sep 17 00:00:00 2001 From: Pascal van Leeuwen Date: Fri, 17 Oct 2025 13:21:23 +0100 Subject: [PATCH 4/4] add proper tag for Go SDK --- go.mod | 3 +-- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d90ff45..633d3cf 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,7 @@ go 1.24 require ( cloud.google.com/go/bigquery v1.69.0 - // TODO_NEXT(@commoddity): Update to concrete version tag when PATH PostgREST client SDK is released - github.com/buildwithgrove/path/portal-db/sdk/go v0.0.0-20251006170518-7fd85f003655 + github.com/buildwithgrove/path/portal-db/sdk/go v0.4.7 github.com/envoyproxy/go-control-plane/envoy v1.32.4 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum index fb8890f..05fa352 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/buildwithgrove/path/portal-db/sdk/go v0.0.0-20251006170518-7fd85f003655 h1:ftU/Aj7YLzDJGZJtxonlpvUuBOGD5uLWViYh6YvCo8A= -github.com/buildwithgrove/path/portal-db/sdk/go v0.0.0-20251006170518-7fd85f003655/go.mod h1:Mqzhv+MOs0YBm34p/Yf/eW+MEpMnyxB1hNtYZNC+fHc= +github.com/buildwithgrove/path/portal-db/sdk/go v0.4.7 h1:EdyA8eHC+eauAV54vkFFPIPzeBmJbbcTeT6AaOpWeG4= +github.com/buildwithgrove/path/portal-db/sdk/go v0.4.7/go.mod h1:Mqzhv+MOs0YBm34p/Yf/eW+MEpMnyxB1hNtYZNC+fHc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=