From 32d6938549bd9fc7e9e2fc7ec0157537bddafcc9 Mon Sep 17 00:00:00 2001 From: Elizabeth Healy <35498075+elizabethhealy@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:50:10 -0500 Subject: [PATCH 1/5] fix(core): Handle multiple modes including entityresolution mode (#1816) ### Proposed Changes * handle sdk_config error throwing when mulltiple modes are present * need to sdk config when running in kas or core without ers ### Checklist - [ ] I have added or updated unit tests - [ ] I have added or updated integration tests (if appropriate) - [ ] I have added or updated documentation ### Testing Instructions --- service/go.mod | 2 +- service/pkg/server/start.go | 14 +- service/pkg/server/start_test.go | 182 +++++++++++++++++- .../pkg/server/testdata/all-no-config.yaml | 100 ++++++++++ 4 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 service/pkg/server/testdata/all-no-config.yaml diff --git a/service/go.mod b/service/go.mod index 8f42d83df..00ad68f06 100644 --- a/service/go.mod +++ b/service/go.mod @@ -151,7 +151,7 @@ require ( github.com/yusufpapurcu/wmi v1.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/service/pkg/server/start.go b/service/pkg/server/start.go index 6624d3117..92c7b49ea 100644 --- a/service/pkg/server/start.go +++ b/service/pkg/server/start.go @@ -157,11 +157,15 @@ func Start(f ...StartOptions) error { oidcconfig *auth.OIDCConfiguration ) - // If the mode is not all or entityresolution, we need to have a valid SDK config + // If the mode is not all, does not include both core and entityresolution, or is not entityresolution on its own, we need to have a valid SDK config // entityresolution does not connect to other services and can run on its own - if !slices.Contains(cfg.Mode, "all") && !slices.Contains(cfg.Mode, "entityresolution") && cfg.SDKConfig == (config.SDKConfig{}) { - logger.Error("mode is not all or entityresolution, but no sdk config provided") - return errors.New("mode is not all or entityresolution, but no sdk config provided") + // core only connects to entityresolution + if !(slices.Contains(cfg.Mode, "all") || // no config required for all mode + (slices.Contains(cfg.Mode, "core") && slices.Contains(cfg.Mode, "entityresolution")) || // or core and entityresolution modes togethor + (slices.Contains(cfg.Mode, "entityresolution") && len(cfg.Mode) == 1)) && // or entityresolution on its own + cfg.SDKConfig == (config.SDKConfig{}) { + logger.Error("mode is not all, entityresolution, or a combination of core and entityresolution, but no sdk config provided") + return errors.New("mode is not all, entityresolution, or a combination of core and entityresolution, but no sdk config provided") } // If client credentials are provided, use them @@ -186,7 +190,7 @@ func Start(f ...StartOptions) error { sdkOptions = append(sdkOptions, sdk.WithCustomCoreConnection(otdf.ConnectRPCInProcess.Conn())) // handle ERS connection for core mode - if slices.Contains(cfg.Mode, "core") { + if slices.Contains(cfg.Mode, "core") && !slices.Contains(cfg.Mode, "entityresolution") { logger.Info("core mode") if cfg.SDKConfig.EntityResolutionConnection.Endpoint == "" { diff --git a/service/pkg/server/start_test.go b/service/pkg/server/start_test.go index db66b50b2..bd54906df 100644 --- a/service/pkg/server/start_test.go +++ b/service/pkg/server/start_test.go @@ -2,10 +2,13 @@ package server import ( "context" + "fmt" "io" "log/slog" "net/http" "net/http/httptest" + "os" + "strings" "testing" "time" @@ -18,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "gopkg.in/yaml.v3" ) type ( @@ -32,9 +36,8 @@ func (t TestService) TestHandler(w http.ResponseWriter, _ *http.Request, _ map[s } } -func mockOpenTDFServer() (*server.OpenTDFServer, error) { +func mockKeycloakServer() *httptest.Server { discoveryURL := "not set yet" - discoveryEndpoint := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { var resp string @@ -62,6 +65,11 @@ func mockOpenTDFServer() (*server.OpenTDFServer, error) { discoveryURL = discoveryEndpoint.URL + return discoveryEndpoint +} + +func mockOpenTDFServer() (*server.OpenTDFServer, error) { + discoveryEndpoint := mockKeycloakServer() // Create new opentdf server return server.NewOpenTDFServer(server.Config{ WellKnownConfigRegister: func(_ string, _ any) error { @@ -82,6 +90,70 @@ func mockOpenTDFServer() (*server.OpenTDFServer, error) { ) } +func updateNestedKey(data map[string]interface{}, path []string, value interface{}) error { + if len(path) == 0 { + return fmt.Errorf("path cannot be empty") + } + + current := data + for i, key := range path[:len(path)-1] { + if next, ok := current[key]; ok { + if nextMap, ok2 := next.(map[string]interface{}); ok2 { + current = nextMap + } else { + return fmt.Errorf("key %s at path level %d is not a map", key, i) + } + } else { + // If the key doesn't exist, initialize a new map + newMap := make(map[string]interface{}) + current[key] = newMap + current = newMap + } + } + + // Set the value at the final key + current[path[len(path)-1]] = value + return nil +} + +func createTempYAMLFileWithNestedChanges(changes map[string]interface{}, originalFilePath string, newFileName string) (string, error) { + // Load the original YAML file + data, err := os.ReadFile(originalFilePath) + if err != nil { + return "", err + } + + var yamlData map[string]interface{} + if err := yaml.Unmarshal(data, &yamlData); err != nil { + return "", err + } + + // Apply all changes + for keyPath, value := range changes { + path := strings.Split(keyPath, ".") // Convert dot notation to slice + if err := updateNestedKey(yamlData, path, value); err != nil { + return "", err + } + } + + // Create a temporary file + tempFile, err := os.CreateTemp("testdata", newFileName) + if err != nil { + return "", err + } + defer tempFile.Close() + + // Write the modified YAML to the temp file + encoder := yaml.NewEncoder(tempFile) + defer encoder.Close() + + if err := encoder.Encode(&yamlData); err != nil { + return "", err + } + + return tempFile.Name(), nil +} + type StartTestSuite struct { suite.Suite } @@ -142,3 +214,109 @@ func (suite *StartTestSuite) Test_Start_When_Extra_Service_Registered_Expect_Res require.NoError(t, err) assert.Equal(t, "hello from test service!", string(respBody)) } + +func (suite *StartTestSuite) Test_Start_Mode_Config_Errors() { + t := suite.T() + discoveryEndpoint := mockKeycloakServer() + originalFilePath := "testdata/all-no-config.yaml" + testCases := []struct { + name string + changes map[string]interface{} + newConfigFile string + expErrorContains string + }{ + {"core without sdk_config", + map[string]interface{}{ + "mode": "core", "server.auth.issuer": discoveryEndpoint.URL}, + "err-core-no-config-*.yaml", "no sdk config provided"}, + {"kas without sdk_config", + map[string]interface{}{ + "mode": "kas", "server.auth.issuer": discoveryEndpoint.URL}, + "err-kas-no-config-*.yaml", "no sdk config provided"}, + {"core with sdk_config without ers endpoint", + map[string]interface{}{ + "mode": "core", "server.auth.issuer": discoveryEndpoint.URL, + "sdk_config.client_id": "opentdf", "sdk_config.client_secret": "opentdf"}, + "err-core-w-config-no-ers-*.yaml", "entityresolution endpoint must be provided in core mode"}, + } + var tempFiles []string + defer func() { + // Cleanup all created temp files + for _, tempFile := range tempFiles { + if err := os.Remove(tempFile); err != nil { + t.Errorf("Failed to remove temp file %s: %v", tempFile, err) + } + } + }() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tempFilePath, err := createTempYAMLFileWithNestedChanges(tc.changes, originalFilePath, tc.newConfigFile) + if err != nil { + t.Fatalf("Failed to create temp YAML file: %v", err) + } + tempFiles = append(tempFiles, tempFilePath) + + err = Start( + WithConfigFile(tempFilePath), + ) + require.Error(t, err) + require.ErrorContains(t, err, tc.expErrorContains) + }) + } +} + +func (suite *StartTestSuite) Test_Start_Mode_Config_Success() { + t := suite.T() + discoveryEndpoint := mockKeycloakServer() + // require.NoError(t, err) + originalFilePath := "testdata/all-no-config.yaml" + testCases := []struct { + name string + changes map[string]interface{} + newConfigFile string + }{ + {"all without sdk_config", + map[string]interface{}{ + "server.auth.issuer": discoveryEndpoint.URL}, + "all-no-config-*.yaml"}, + {"core,entityresolution without sdk_config", + map[string]interface{}{ + "mode": "core,entityresolution", "server.auth.issuer": discoveryEndpoint.URL}, + "all-no-config-*.yaml"}, + {"core,entityresolution,kas without sdk_config", + map[string]interface{}{ + "mode": "core,entityresolution,kas", "server.auth.issuer": discoveryEndpoint.URL}, + "all-no-config-*.yaml"}, + {"core with correct sdk_config", + map[string]interface{}{ + "mode": "core", "server.auth.issuer": discoveryEndpoint.URL, + "sdk_config.client_id": "opentdf", "sdk_config.client_secret": "opentdf", + "sdk_config.entityresolution.endpoint": "http://localhost:8181", "sdk_config.entityresolution.plaintext": "true"}, + "core-w-config-correct-*.yaml"}, + } + var tempFiles []string + defer func() { + // Cleanup all created temp files + for _, tempFile := range tempFiles { + if err := os.Remove(tempFile); err != nil { + t.Errorf("Failed to remove temp file %s: %v", tempFile, err) + } + } + }() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tempFilePath, err := createTempYAMLFileWithNestedChanges(tc.changes, originalFilePath, tc.newConfigFile) + if err != nil { + t.Fatalf("Failed to create temp YAML file: %v", err) + } + tempFiles = append(tempFiles, tempFilePath) + + err = Start( + WithConfigFile(tempFilePath), + ) + // require that it got past the service config and mode setup + // expected error when trying to establish db connection + require.ErrorContains(t, err, "failed to connect to database") + }) + } +} diff --git a/service/pkg/server/testdata/all-no-config.yaml b/service/pkg/server/testdata/all-no-config.yaml new file mode 100644 index 000000000..ff4e1f299 --- /dev/null +++ b/service/pkg/server/testdata/all-no-config.yaml @@ -0,0 +1,100 @@ + +mode: all +logger: + level: debug + type: text + output: stdout +services: + kas: + keyring: + - kid: e1 + alg: ec:secp256r1 + - kid: e1 + alg: ec:secp256r1 + legacy: true + - kid: r1 + alg: rsa:2048 + - kid: r1 + alg: rsa:2048 + legacy: true + entityresolution: + log_level: info + url: http://localhost:8888/auth + clientid: 'tdf-entity-resolution' + clientsecret: 'secret' + realm: 'opentdf' + legacykeycloak: true + inferid: + from: + email: true + username: true +server: + tls: + enabled: false + cert: ./keys/platform.crt + key: ./keys/platform-key.pem + auth: + enabled: true + enforceDPoP: false + public_client_id: 'opentdf-public' + audience: 'http://localhost:8080' + issuer: http://localhost:8888/auth/realms/opentdf + policy: + ## Dot notation is used to access nested claims (i.e. realm_access.roles) + # Claim that represents the user (i.e. email) + username_claim: # preferred_username + # That claim to access groups (i.e. realm_access.roles) + groups_claim: # realm_access.roles + ## Extends the builtin policy + extension: | + g, opentdf-admin, role:admin + g, opentdf-standard, role:standard + ## Custom policy that overrides builtin policy (see examples https://github.com/casbin/casbin/tree/master/examples) + csv: #| + # p, role:admin, *, *, allow + ## Custom model (see https://casbin.org/docs/syntax-for-models/) + model: #| + # [request_definition] + # r = sub, res, act, obj + # + # [policy_definition] + # p = sub, res, act, obj, eft + # + # [role_definition] + # g = _, _ + # + # [policy_effect] + # e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + # + # [matchers] + # m = g(r.sub, p.sub) && globOrRegexMatch(r.res, p.res) && globOrRegexMatch(r.act, p.act) && globOrRegexMatch(r.obj, p.obj) + cors: + enabled: false + # "*" to allow any origin or a specific domain like "https://yourdomain.com" + allowedorigins: + - '*' + # List of methods. Examples: "GET,POST,PUT" + allowedmethods: + - GET + - POST + - PATCH + - PUT + - DELETE + - OPTIONS + # List of headers that are allowed in a request + allowedheaders: + - ACCEPT + - Authorization + - Content-Type + - X-CSRF-Token + - X-Request-ID + # List of response headers that browsers are allowed to access + exposedheaders: + - Link + # Sets whether credentials are included in the CORS request + allowcredentials: true + # Sets the maximum age (in seconds) of a specific CORS preflight request + maxage: 3600 + grpc: + reflectionEnabled: true # Default is false + port: 8080 From 260d73267a6d89ba0ec8b39e62a4fa304db79a51 Mon Sep 17 00:00:00 2001 From: "opentdf-automation[bot]" <149537512+opentdf-automation[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:12:34 -0500 Subject: [PATCH 2/5] chore(main): release service 0.4.33 (#1814) :robot: I have created a release *beep* *boop* --- ## [0.4.33](https://github.com/opentdf/platform/compare/service/v0.4.32...service/v0.4.33) (2024-12-06) ### Bug Fixes * **core:** Allow more users to rewrap ([#1813](https://github.com/opentdf/platform/issues/1813)) ([4d47475](https://github.com/opentdf/platform/commit/4d474750c20a9a6fe0f00487195851a606e24076)) * **core:** Handle multiple modes including entityresolution mode ([#1816](https://github.com/opentdf/platform/issues/1816)) ([32d6938](https://github.com/opentdf/platform/commit/32d6938549bd9fc7e9e2fc7ec0157537bddafcc9)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: opentdf-automation[bot] <149537512+opentdf-automation[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- service/CHANGELOG.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index de9c2322a..b6abad871 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -4,5 +4,5 @@ "lib/flattening": "0.1.2", "protocol/go": "0.2.22", "sdk": "0.3.23", - "service": "0.4.32" + "service": "0.4.33" } diff --git a/service/CHANGELOG.md b/service/CHANGELOG.md index 992802956..bc1238cc1 100644 --- a/service/CHANGELOG.md +++ b/service/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.4.33](https://github.com/opentdf/platform/compare/service/v0.4.32...service/v0.4.33) (2024-12-06) + + +### Bug Fixes + +* **core:** Allow more users to rewrap ([#1813](https://github.com/opentdf/platform/issues/1813)) ([4d47475](https://github.com/opentdf/platform/commit/4d474750c20a9a6fe0f00487195851a606e24076)) +* **core:** Handle multiple modes including entityresolution mode ([#1816](https://github.com/opentdf/platform/issues/1816)) ([32d6938](https://github.com/opentdf/platform/commit/32d6938549bd9fc7e9e2fc7ec0157537bddafcc9)) + ## [0.4.32](https://github.com/opentdf/platform/compare/service/v0.4.31...service/v0.4.32) (2024-12-04) From 5b9f0541f39c6141ea060d699146482959fb32f7 Mon Sep 17 00:00:00 2001 From: Sean Trantalis <18211470+strantalis@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:27:52 -0500 Subject: [PATCH 3/5] fix(core): properly chain grpc-gateway middleware order (#1820) ### Proposed Changes Fixes the grpc-gateway middleware chaining order. Handling of a cors pre-flight call before moving onto auth middleware. ### Checklist - [ ] I have added or updated unit tests - [ ] I have added or updated integration tests (if appropriate) - [ ] I have added or updated documentation ### Testing Instructions --- opentdf-dev.yaml | 13 ++++----- opentdf-ers-mode.yaml | 13 ++++----- opentdf-example.yaml | 13 ++++----- service/internal/server/server.go | 21 ++++++++------ test/service-start.bats | 47 ++++++++++++++++++++++++++++++- 5 files changed, 76 insertions(+), 31 deletions(-) diff --git a/opentdf-dev.yaml b/opentdf-dev.yaml index 79c0e2b86..f93d7d630 100644 --- a/opentdf-dev.yaml +++ b/opentdf-dev.yaml @@ -24,9 +24,9 @@ services: entityresolution: log_level: info url: http://localhost:8888/auth - clientid: 'tdf-entity-resolution' - clientsecret: 'secret' - realm: 'opentdf' + clientid: "tdf-entity-resolution" + clientsecret: "secret" + realm: "opentdf" legacykeycloak: true inferid: from: @@ -45,8 +45,8 @@ server: auth: enabled: true enforceDPoP: false - public_client_id: 'opentdf-public' - audience: 'http://localhost:8080' + public_client_id: "opentdf-public" + audience: "http://localhost:8080" issuer: http://localhost:8888/auth/realms/opentdf policy: ## Dot notation is used to access nested claims (i.e. realm_access.roles) @@ -78,10 +78,9 @@ server: # [matchers] # m = g(r.sub, p.sub) && globOrRegexMatch(r.res, p.res) && globOrRegexMatch(r.act, p.act) && globOrRegexMatch(r.obj, p.obj) cors: - enabled: false # "*" to allow any origin or a specific domain like "https://yourdomain.com" allowedorigins: - - '*' + - "*" # List of methods. Examples: "GET,POST,PUT" allowedmethods: - GET diff --git a/opentdf-ers-mode.yaml b/opentdf-ers-mode.yaml index 019b11496..22ddb798f 100644 --- a/opentdf-ers-mode.yaml +++ b/opentdf-ers-mode.yaml @@ -9,9 +9,9 @@ services: entityresolution: log_level: info url: http://localhost:8888/auth - clientid: 'tdf-entity-resolution' - clientsecret: 'secret' - realm: 'opentdf' + clientid: "tdf-entity-resolution" + clientsecret: "secret" + realm: "opentdf" legacykeycloak: true inferid: from: @@ -21,8 +21,8 @@ server: auth: enabled: true enforceDPoP: false - public_client_id: 'opentdf-public' - audience: 'http://localhost:8080' + public_client_id: "opentdf-public" + audience: "http://localhost:8080" issuer: http://localhost:8888/auth/realms/opentdf policy: ## Default policy for all requests @@ -61,10 +61,9 @@ server: # [matchers] # m = g(r.sub, p.sub) && globOrRegexMatch(r.res, p.res) && globOrRegexMatch(r.act, p.act) && globOrRegexMatch(r.obj, p.obj) cors: - enabled: false # "*" to allow any origin or a specific domain like "https://yourdomain.com" allowedorigins: - - '*' + - "*" # List of methods. Examples: "GET,POST,PUT" allowedmethods: - GET diff --git a/opentdf-example.yaml b/opentdf-example.yaml index 993c07f96..311b1e1d1 100644 --- a/opentdf-example.yaml +++ b/opentdf-example.yaml @@ -15,9 +15,9 @@ services: rsacertid: r1 entityresolution: url: http://keycloak:8888/auth - clientid: 'tdf-entity-resolution' - clientsecret: 'secret' - realm: 'opentdf' + clientid: "tdf-entity-resolution" + clientsecret: "secret" + realm: "opentdf" legacykeycloak: true inferid: from: @@ -32,8 +32,8 @@ server: auth: enabled: true enforceDPoP: false - public_client_id: 'opentdf-public' - audience: 'http://localhost:8080' + public_client_id: "opentdf-public" + audience: "http://localhost:8080" issuer: http://keycloak:8888/auth/realms/opentdf policy: ## Dot notation is used to access nested claims (i.e. realm_access.roles) @@ -65,10 +65,9 @@ server: # [matchers] # m = g(r.sub, p.sub) && globOrRegexMatch(r.res, p.res) && globOrRegexMatch(r.act, p.act) && globOrRegexMatch(r.obj, p.obj) cors: - enabled: false # "*" to allow any origin or a specific domain like "https://yourdomain.com" allowedorigins: - - '*' + - "*" # List of methods. Examples: "GET,POST,PUT" allowedmethods: - GET diff --git a/service/internal/server/server.go b/service/internal/server/server.go index 5f1409117..8dac4f47d 100644 --- a/service/internal/server/server.go +++ b/service/internal/server/server.go @@ -96,11 +96,12 @@ type CORSConfig struct { // Enable CORS for the server (default: true) Enabled bool `mapstructure:"enabled" json:"enabled" default:"true"` AllowedOrigins []string `mapstructure:"allowedorigins" json:"allowedorigins"` - AllowedMethods []string `mapstructure:"allowedmethods" json:"allowedmethods"` - AllowedHeaders []string `mapstructure:"allowedheaders" json:"allowedheaders"` + AllowedMethods []string `mapstructure:"allowedmethods" json:"allowedmethods" default:"[\"GET\",\"POST\",\"PATCH\",\"DELETE\",\"OPTIONS\"]"` + AllowedHeaders []string `mapstructure:"allowedheaders" json:"allowedheaders" default:"[\"Accept\",\"Content-Type\",\"Content-Length\",\"Accept-Encoding\",\"X-CSRF-Token\",\"Authorization\",\"X-Requested-With\",\"Dpop\"]"` ExposedHeaders []string `mapstructure:"exposedheaders" json:"exposedheaders"` AllowCredentials bool `mapstructure:"allowcredentials" json:"allowedcredentials" default:"true"` MaxAge int `mapstructure:"maxage" json:"maxage" default:"3600"` + Debug bool `mapstructure:"debug" json:"debug"` } type ConnectRPC struct { @@ -264,6 +265,14 @@ func newHTTPServer(c Config, connectRPC http.Handler, originalGrpcGateway http.H originalGrpcGateway.ServeHTTP(grpcRW, r) }) + // Add authN interceptor to extra handlers + if c.Auth.Enabled { + grpcGateway = a.MuxHandler(grpcGateway) + } else { + l.Error("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP set `enforceDPoP = false`") + } + + // Note: The grpc-gateway handlers are getting chained together in reverse. So the last handler is the first to be called. // CORS if c.CORS.Enabled { corsHandler := cors.New(cors.Options{ @@ -283,6 +292,7 @@ func newHTTPServer(c Config, connectRPC http.Handler, originalGrpcGateway http.H ExposedHeaders: c.CORS.ExposedHeaders, AllowCredentials: c.CORS.AllowCredentials, MaxAge: c.CORS.MaxAge, + Debug: c.CORS.Debug, }) // Apply CORS to connectRPC and extra handlers @@ -290,13 +300,6 @@ func newHTTPServer(c Config, connectRPC http.Handler, originalGrpcGateway http.H grpcGateway = corsHandler.Handler(grpcGateway) } - // Add authN interceptor to extra handlers - if c.Auth.Enabled { - grpcGateway = a.MuxHandler(grpcGateway) - } else { - l.Error("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP set `enforceDPoP = false`") - } - // Enable pprof if c.EnablePprof { grpcGateway = pprofHandler(grpcGateway) diff --git a/test/service-start.bats b/test/service-start.bats index dc590ffda..905fa1a78 100755 --- a/test/service-start.bats +++ b/test/service-start.bats @@ -78,4 +78,49 @@ echo "$output" [ $status = 0 ] [ $(jq -r .status <<<"${output}") = SERVING ] -} \ No newline at end of file +} + +@test "GRPC-Gateway: Validate CORS" { + run curl -X OPTIONS -v -s "https://localhost:8080/healthz" -H "Origin: https://example.com" -H "Access-Control-Request-Method: GET" + echo "$output" + [ $(grep -c "access-control-allow-origin: https://example.com" <<<"${output}") -eq 1 ] + [ $(grep -c "access-control-allow-methods: GET" <<<"${output}") -eq 1 ] + [ $(grep -c "access-control-allow-credentials: true" <<<"${output}") -eq 1 ] +} + +@test "GRPC-Gateway: Reject non-accepted headers" { + run curl -X OPTIONS -v -s "https://localhost:8080/healthz" \ + -H "Origin: https://example.com" \ + -H "Access-Control-Request-Method: GET" \ + -H "Access-Control-Request-Headers: X-Not-Allowed-Header" + + echo "$output" + # Verify the headers are not present in the response + [ $(grep -c "access-control-allow-origin: https://example.com" <<<"${output}") -eq 0 ] + [ $(grep -c "access-control-allow-methods: GET" <<<"${output}") -eq 0 ] + [ $(grep -c "access-control-allow-credentials: true" <<<"${output}") -eq 0 ] + [ $(grep -c "access-control-max-age: 3600" <<<"${output}") -eq 0 ] +} + +@test "Connect-RPC: Validate CORS" { + run curl -X OPTIONS -v -s "https://localhost:8080/grpc.health.v1.Health/Check" -H "Origin: https://example.com" -H "Access-Control-Request-Method: GET" + echo "$output" + [ $(grep -c "access-control-allow-origin: https://example.com" <<<"${output}") -eq 1 ] + [ $(grep -c "access-control-allow-methods: GET" <<<"${output}") -eq 1 ] + [ $(grep -c "access-control-allow-credentials: true" <<<"${output}") -eq 1 ] +} + +@test "Connect-RPC: Reject non-accepted headers" { + run curl -X OPTIONS -v -s "https://localhost:8080/grpc.health.v1.Health/Check" \ + -H "Origin: https://example.com" \ + -H "Access-Control-Request-Method: GET" \ + -H "Access-Control-Request-Headers: X-Not-Allowed-Header" + + echo "$output" + # Verify the headers are not present in the response + [ $(grep -c "access-control-allow-origin: https://example.com" <<<"${output}") -eq 0 ] + [ $(grep -c "access-control-allow-methods: GET" <<<"${output}") -eq 0 ] + [ $(grep -c "access-control-allow-credentials: true" <<<"${output}") -eq 0 ] + [ $(grep -c "access-control-max-age: 3600" <<<"${output}") -eq 0 ] +} + From 7d55a909ac03e5a1234709d9051f906d9efd469e Mon Sep 17 00:00:00 2001 From: "opentdf-automation[bot]" <149537512+opentdf-automation[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 17:52:36 +0000 Subject: [PATCH 4/5] chore(main): release service 0.4.34 (#1821) :robot: I have created a release *beep* *boop* --- ## [0.4.34](https://github.com/opentdf/platform/compare/service/v0.4.33...service/v0.4.34) (2024-12-11) ### Bug Fixes * **core:** properly chain grpc-gateway middleware order ([#1820](https://github.com/opentdf/platform/issues/1820)) ([5b9f054](https://github.com/opentdf/platform/commit/5b9f0541f39c6141ea060d699146482959fb32f7)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: opentdf-automation[bot] <149537512+opentdf-automation[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- service/CHANGELOG.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b6abad871..4fc0c550e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -4,5 +4,5 @@ "lib/flattening": "0.1.2", "protocol/go": "0.2.22", "sdk": "0.3.23", - "service": "0.4.33" + "service": "0.4.34" } diff --git a/service/CHANGELOG.md b/service/CHANGELOG.md index bc1238cc1..e7a412f90 100644 --- a/service/CHANGELOG.md +++ b/service/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.4.34](https://github.com/opentdf/platform/compare/service/v0.4.33...service/v0.4.34) (2024-12-11) + + +### Bug Fixes + +* **core:** properly chain grpc-gateway middleware order ([#1820](https://github.com/opentdf/platform/issues/1820)) ([5b9f054](https://github.com/opentdf/platform/commit/5b9f0541f39c6141ea060d699146482959fb32f7)) + ## [0.4.33](https://github.com/opentdf/platform/compare/service/v0.4.32...service/v0.4.33) (2024-12-06) From 7828aeb78a686d05f32e03a5f8977437a631daf0 Mon Sep 17 00:00:00 2001 From: Paul Flynn <43211074+pflynn-virtru@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:26:07 -0500 Subject: [PATCH 5/5] chore: Remove unused keycloakdb service from docker-compose (#1817) The keycloakdb service has been removed from the docker-compose file as it was not necessary for the current setup. This change helps streamline the configuration and prevents unnecessary resource allocation ### Proposed Changes * Keycloak runs in dev mode, no need for Postgres ### Checklist - [ ] I have added or updated unit tests - [ ] I have added or updated integration tests (if appropriate) - [ ] I have added or updated documentation ### Testing Instructions --- docker-compose.yaml | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 01d1bed3c..d7b04ebac 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -48,7 +48,7 @@ services: - "8888:8888" - "8443:8443" healthcheck: - test: + test: - CMD-SHELL - | [ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { @@ -66,23 +66,10 @@ services: java.net.HttpURLConnection conn = (java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection(); System.exit(java.net.HttpURLConnection.HTTP_OK == conn.getResponseCode() ? 0 : 1); } - }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java https://localhost:9001/auth/health/live + }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java https://localhost:9001/auth/health/live timeout: 10s retries: 3 start_period: 2m - keycloakdb: - image: postgres:15-alpine - restart: always - user: postgres - environment: - POSTGRES_PASSWORD: changeme - POSTGRES_USER: postgres - POSTGRES_DB: keycloak - healthcheck: - test: ["CMD-SHELL", "pg_isready"] - interval: 5s - timeout: 5s - retries: 10 opentdfdb: image: postgres:15-alpine restart: always