diff --git a/Contributing.md b/Contributing.md index 93f67b449..ad5818519 100644 --- a/Contributing.md +++ b/Contributing.md @@ -16,7 +16,7 @@ For end-users/consumers, see [here](./Consuming.md). 1. Note: You will have to add the ``localhost.crt`` as a trusted certificate to do TLS authentication at ``localhost:8443``. 3. Create an OpenTDF config file: `opentdf.yaml` 1. The `opentdf-dev.yaml` file is the more secure starting point, but you will likely need to modify it to match your environment. This configuration is recommended as it is more secure but it does require valid development keypairs. - 2. The `opentdf-example-no-kas.yaml` file is simpler to run but less secure. This file configures the platform to startup without a KAS instances and without endpoint authentication. + 2. The `opentdf-core-mode.yaml` file is simpler to run but less secure. This file configures the platform to startup without a KAS instances, without a built-in ERS instance, and without endpoint authentication. 4. Provision keycloak: `go run github.com/opentdf/platform/service provision keycloak`. Updates the local Keycloak configuration for local testing and development by creating a realm, roles, a client, and users. 5. Run the server: `go run github.com/opentdf/platform/service start`. Runs the OpenTDF platform capabilities as a monolithic service. 1. _Alt_ use the hot-reload development environment `air` diff --git a/docs/configuration.md b/docs/configuration.md index f55551fbd..3810e4245 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -34,8 +34,12 @@ Root level key `sdk_config` | Field | Description | Default | Environment Variable | | -------- | -------------| -------- | -------------------- | -| `endpoint` | The core platform endpoint to connect to | | OPENTDF_SDK_CONFIG_ENDPOINT | -| `plaintext` | Use a plaintext grpc connection | `false` | OPENTDF_SDK_CONFIG_PLAINTEXT | +| `core.endpoint` | The core platform endpoint to connect to | | OPENTDF_SDK_CONFIG_ENDPOINT | +| `core.plaintext` | Use a plaintext grpc connection | `false` | OPENTDF_SDK_CONFIG_PLAINTEXT | +| `core.insecure` | Use an insecure tls connection | `false` | | +| `entityresolution.endpoint` | The entityresolution endpoint to connect to | | | +| `entityresolution.plaintext` | Use a plaintext ERS grpc connection | `false` | | +| `entityresolution.insecure` | Use an insecure tls connection | `false` | | | `client_id` | OAuth client id | | OPENTDF_SDK_CONFIG_CLIENT_ID | | `client_secret` | The clients credentials | | OPENTDF_SDK_CONFIG_CLIENT_SECRET | diff --git a/opentdf-example-no-kas.yaml b/opentdf-core-mode.yaml similarity index 74% rename from opentdf-example-no-kas.yaml rename to opentdf-core-mode.yaml index aee0b0925..ef440c64b 100644 --- a/opentdf-example-no-kas.yaml +++ b/opentdf-core-mode.yaml @@ -1,4 +1,12 @@ +# configures the platform to startup without a KAS instances, without a built-in ERS instance, and without endpoint authentication +# build off of this config file if you are running your ERS and KAS instances seperately or if you only need the policy features mode: core +sdk_config: + entityresolution: + endpoint: http://localhost:8181 + plaintext: true + client_id: opentdf + client_secret: secret logger: level: debug type: text @@ -44,4 +52,4 @@ server: maxage: 3600 grpc: reflectionEnabled: true # Default is false - port: 8080 + port: 8383 \ No newline at end of file diff --git a/opentdf-ers-mode.yaml b/opentdf-ers-mode.yaml new file mode 100644 index 000000000..019b11496 --- /dev/null +++ b/opentdf-ers-mode.yaml @@ -0,0 +1,92 @@ +# configures the platform to run just the entity resolution service and the well-known service +# primarily to be used for testing and development of external ERS connections +mode: entityresolution +logger: + level: debug + type: text + output: stdout +services: + 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: + auth: + enabled: true + enforceDPoP: false + public_client_id: 'opentdf-public' + audience: 'http://localhost:8080' + issuer: http://localhost:8888/auth/realms/opentdf + policy: + ## Default policy for all requests + default: #"role:standard" + ## Dot notation is used to access nested claims (i.e. realm_access.roles) + claim: # realm_access.roles + ## Maps the external role to the opentdf role + ## Note: left side is used in the policy, right side is the external role + map: + # standard: opentdf-standard + # admin: opentdf-admin + # org-admin: opentdf-org-admin + + ## Custom policy (see examples https://github.com/casbin/casbin/tree/master/examples) + csv: #| + # p, role:org-admin, policy:attributes, *, *, allow + # p, role:org-admin, policy:subject-mappings, *, *, allow + # p, role:org-admin, policy:resource-mappings, *, *, allow + # p, role:org-admin, policy:kas-registry, *, *, allow + # p, role:org-admin, policy:unsafe, *, *, 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: 8282 diff --git a/opentdf-kas-mode.yaml b/opentdf-kas-mode.yaml new file mode 100644 index 000000000..9340ea1c1 --- /dev/null +++ b/opentdf-kas-mode.yaml @@ -0,0 +1,110 @@ +# configures the platform to run only the KAS and well known service +# build off this config file if you intend on running a (or multiple) seperate kas instance(s) +mode: kas +sdk_config: + core: + endpoint: http://localhost:8080 + plaintext: true + client_id: opentdf + client_secret: secret +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 +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: + ## Default policy for all requests + default: #"role:standard" + ## Dot notation is used to access nested claims (i.e. realm_access.roles) + claim: # realm_access.roles + ## Maps the external role to the opentdf role + ## Note: left side is used in the policy, right side is the external role + map: + # standard: opentdf-standard + # admin: opentdf-admin + + ## Custom 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 + cryptoProvider: + type: standard + standard: + keys: + - kid: r1 + alg: rsa:2048 + private: kas-private.pem + cert: kas-cert.pem + - kid: e1 + alg: ec:secp256r1 + private: kas-ec-private.pem + cert: kas-ec-cert.pem + port: 8181 diff --git a/sdk/options.go b/sdk/options.go index cef58880d..3ec1da618 100644 --- a/sdk/options.go +++ b/sdk/options.go @@ -35,6 +35,7 @@ type config struct { customAccessTokenSource auth.AccessTokenSource oauthAccessTokenSource oauth2.TokenSource coreConn *grpc.ClientConn + entityResolutionConn *grpc.ClientConn collectionStore *collectionStore } @@ -135,10 +136,9 @@ func WithCustomAuthorizationConnection(conn *grpc.ClientConn) Option { } } -// Deprecated: Use WithCustomCoreConnection instead func WithCustomEntityResolutionConnection(conn *grpc.ClientConn) Option { return func(c *config) { - c.coreConn = conn + c.entityResolutionConn = conn } } diff --git a/sdk/sdk.go b/sdk/sdk.go index 7202bea27..90ca9b06f 100644 --- a/sdk/sdk.go +++ b/sdk/sdk.go @@ -74,6 +74,7 @@ type SDK struct { func New(platformEndpoint string, opts ...Option) (*SDK, error) { var ( platformConn *grpc.ClientConn // Connection to the platform + ersConn *grpc.ClientConn // Connection to ERS (possibly remote) err error ) @@ -169,6 +170,12 @@ func New(platformEndpoint string, opts ...Option) (*SDK, error) { } } + if cfg.entityResolutionConn != nil { + ersConn = cfg.entityResolutionConn + } else { + ersConn = platformConn + } + return &SDK{ config: *cfg, collectionStore: cfg.collectionStore, @@ -183,7 +190,7 @@ func New(platformEndpoint string, opts ...Option) (*SDK, error) { Unsafe: unsafe.NewUnsafeServiceClient(platformConn), KeyAccessServerRegistry: kasregistry.NewKeyAccessServerRegistryServiceClient(platformConn), Authorization: authorization.NewAuthorizationServiceClient(platformConn), - EntityResoution: entityresolution.NewEntityResolutionServiceClient(platformConn), + EntityResoution: entityresolution.NewEntityResolutionServiceClient(ersConn), wellknownConfiguration: wellknownconfiguration.NewWellKnownServiceClient(platformConn), }, nil } diff --git a/service/go.mod b/service/go.mod index b0c228bc7..ba685d2cb 100644 --- a/service/go.mod +++ b/service/go.mod @@ -24,7 +24,7 @@ require ( github.com/opentdf/platform/lib/flattening v0.1.1 github.com/opentdf/platform/lib/ocrypto v0.1.7 github.com/opentdf/platform/protocol/go v0.2.20 - github.com/opentdf/platform/sdk v0.3.21 + github.com/opentdf/platform/sdk v0.3.22 github.com/pressly/goose/v3 v3.19.1 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.18.2 diff --git a/service/go.sum b/service/go.sum index 8bdf7ce71..1378d93f5 100644 --- a/service/go.sum +++ b/service/go.sum @@ -287,8 +287,8 @@ github.com/opentdf/platform/lib/ocrypto v0.1.7 h1:IcCYRrwmMqntqUE8frmUDg5EZ0WMdl github.com/opentdf/platform/lib/ocrypto v0.1.7/go.mod h1:4bhKPbRFzURMerH5Vr/LlszHvcoXQbfJXa0bpY7/7yg= github.com/opentdf/platform/protocol/go v0.2.20 h1:FPU1ZcXvPm/QeE2nqgbD/HMTOCICQSD0DoncQbAZ1ws= github.com/opentdf/platform/protocol/go v0.2.20/go.mod h1:TWIuf387VeR3q0TL4nAMKQTWEqqID+8Yjao76EX9Dto= -github.com/opentdf/platform/sdk v0.3.21 h1:18oZk8t32luXBL2lhRa3qvjTY17Y3PmA0Wp1F8tdkqc= -github.com/opentdf/platform/sdk v0.3.21/go.mod h1:KpT/m5zXQ19WqhGePKfIC39Ly8LOipKdKGbJ1B/59a8= +github.com/opentdf/platform/sdk v0.3.22 h1:nxmu7i+dmKuRQKVi5EIjOVdEFzzu/zkaA5LmGPPtPzw= +github.com/opentdf/platform/sdk v0.3.22/go.mod h1:KpT/m5zXQ19WqhGePKfIC39Ly8LOipKdKGbJ1B/59a8= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= diff --git a/service/internal/config/config.go b/service/internal/config/config.go index 49b93df94..a5f3f1e33 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -43,11 +43,11 @@ type Config struct { // SDKConfig represents the configuration for the SDK. type SDKConfig struct { - // Endpoint is the URL of the Core Platform endpoint. - Endpoint string `mapstructure:"endpoint" json:"endpoint"` + // Connection to the Core Platform + CorePlatformConnection Connection `mapstructure:"core" json:"core"` - // Plaintext specifies whether the SDK should use plaintext communication. - Plaintext bool `mapstructure:"plaintext" json:"plaintext" default:"false" validate:"boolean"` + // Connection to an ERS if not in the core platform + EntityResolutionConnection Connection `mapstructure:"entityresolution" json:"entityresolution"` // ClientID is the client ID used for client credentials grant. // It is required together with ClientSecret. @@ -58,6 +58,17 @@ type SDKConfig struct { ClientSecret string `mapstructure:"client_secret" json:"client_secret" validate:"required_with=ClientID"` } +type Connection struct { + // Endpoint is the URL of the platform or service. + Endpoint string `mapstructure:"endpoint" json:"endpoint"` + + // Plaintext specifies whether the SDK should use plaintext communication. + Plaintext bool `mapstructure:"plaintext" json:"plaintext" default:"false" validate:"boolean"` + + // Insecure specifies whether the SDK should use insecure TLS communication. + Insecure bool `mapstructure:"insecure" json:"insecure" default:"false" validate:"boolean"` +} + type Error string func (e Error) Error() string { @@ -137,8 +148,14 @@ func (c *Config) LogValue() slog.Value { func (c SDKConfig) LogValue() slog.Value { return slog.GroupValue( - slog.String("endpoint", c.Endpoint), - slog.Bool("plaintext", c.Plaintext), + slog.Group("core", + "endpoint", c.CorePlatformConnection.Endpoint, + "plaintext", c.CorePlatformConnection.Plaintext, + "insecure", c.CorePlatformConnection.Insecure), + slog.Group("entityresolution", + "endpoint", c.EntityResolutionConnection.Endpoint, + "plaintext", c.EntityResolutionConnection.Plaintext, + "insecure", c.EntityResolutionConnection.Insecure), slog.String("client_id", c.ClientID), slog.String("client_secret", "[REDACTED]"), ) diff --git a/service/pkg/server/services.go b/service/pkg/server/services.go index 637f96c1d..26b50e5e6 100644 --- a/service/pkg/server/services.go +++ b/service/pkg/server/services.go @@ -30,6 +30,7 @@ const ( modeALL = "all" modeCore = "core" modeKAS = "kas" + modeERS = "entityresolution" modeEssential = "essential" serviceKAS = "kas" @@ -76,7 +77,6 @@ func registerCoreServices(reg serviceregistry.Registry, mode []string) ([]string case "core": registeredServices = append(registeredServices, []string{servicePolicy, serviceAuthorization, serviceWellKnown}...) services = append(services, []serviceregistry.IService{ - entityresolution.NewRegistration(), authorization.NewRegistration(), wellknown.NewRegistration(), }...) @@ -87,6 +87,12 @@ func registerCoreServices(reg serviceregistry.Registry, mode []string) ([]string if err := reg.RegisterService(kas.NewRegistration(), modeKAS); err != nil { return nil, err //nolint:wrapcheck // We are all friends here } + case "entityresolution": + // If the mode is "entityresolution", register only the ERS service + registeredServices = append(registeredServices, serviceEntityResolution) + if err := reg.RegisterService(entityresolution.NewRegistration(), modeERS); err != nil { + return nil, err //nolint:wrapcheck // We are all friends here + } default: continue } diff --git a/service/pkg/server/services_test.go b/service/pkg/server/services_test.go index 97248da3a..130be673a 100644 --- a/service/pkg/server/services_test.go +++ b/service/pkg/server/services_test.go @@ -150,11 +150,6 @@ func (suite *ServiceTestSuite) Test_RegisterCoreServices_In_Mode_Core_Expect_Cor suite.Require().NoError(err) suite.Len(wellKnown.Services, 1) suite.Equal(modeCore, wellKnown.Mode) - - ers, err := registry.GetNamespace(serviceEntityResolution) - suite.Require().NoError(err) - suite.Len(ers.Services, 1) - suite.Equal(modeCore, ers.Mode) } // Register core and kas services @@ -182,11 +177,38 @@ func (suite *ServiceTestSuite) Test_RegisterServices_In_Mode_Core_Plus_Kas_Expec suite.Require().NoError(err) suite.Len(wellKnown.Services, 1) suite.Equal(modeCore, wellKnown.Mode) +} + +// Register core and kas and ERS services +func (suite *ServiceTestSuite) Test_RegisterServices_In_Mode_Core_Plus_Kas_Expect_Core_And_Kas_And_ERS_Services_Registered() { + registry := serviceregistry.NewServiceRegistry() + _, err := registerCoreServices(registry, []string{modeCore, modeKAS, modeERS}) + suite.Require().NoError(err) + + authz, err := registry.GetNamespace(serviceAuthorization) + suite.Require().NoError(err) + suite.Len(authz.Services, 1) + suite.Equal(modeCore, authz.Mode) + + kas, err := registry.GetNamespace(serviceKAS) + suite.Require().NoError(err) + suite.Len(kas.Services, 1) + suite.Equal(modeKAS, kas.Mode) + + policy, err := registry.GetNamespace(servicePolicy) + suite.Require().NoError(err) + suite.Len(policy.Services, 6) + suite.Equal(modeCore, policy.Mode) + + wellKnown, err := registry.GetNamespace(serviceWellKnown) + suite.Require().NoError(err) + suite.Len(wellKnown.Services, 1) + suite.Equal(modeCore, wellKnown.Mode) ers, err := registry.GetNamespace(serviceEntityResolution) suite.Require().NoError(err) suite.Len(ers.Services, 1) - suite.Equal(modeCore, ers.Mode) + suite.Equal(modeERS, ers.Mode) } func (suite *ServiceTestSuite) TestStartServicesWithVariousCases() { diff --git a/service/pkg/server/start.go b/service/pkg/server/start.go index 815f71b8e..d10821d96 100644 --- a/service/pkg/server/start.go +++ b/service/pkg/server/start.go @@ -2,14 +2,20 @@ package server import ( "context" + "crypto/tls" "errors" "fmt" "log/slog" + "net" + "net/url" "os" "os/signal" "syscall" + "github.com/opentdf/platform/lib/ocrypto" "github.com/opentdf/platform/sdk" + sdkauth "github.com/opentdf/platform/sdk/auth" + "github.com/opentdf/platform/sdk/auth/oauth" "github.com/opentdf/platform/service/internal/auth" "github.com/opentdf/platform/service/internal/config" "github.com/opentdf/platform/service/internal/server" @@ -17,6 +23,9 @@ import ( "github.com/opentdf/platform/service/pkg/serviceregistry" wellknown "github.com/opentdf/platform/service/wellknownconfiguration" "golang.org/x/exp/slices" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" ) const devModeMessage = ` @@ -27,6 +36,7 @@ const devModeMessage = ` ██████╔╝███████╗ ╚████╔╝ ███████╗███████╗╚██████╔╝██║ ██║ ╚═╝ ██║███████╗██║ ╚████║ ██║ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗ ╚═════╝ ╚══════╝ ╚═══╝ ╚══════╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ` +const dpopKeySize = 2048 func Start(f ...StartOptions) error { startConfig := StartConfig{} @@ -136,19 +146,21 @@ func Start(f ...StartOptions) error { var ( sdkOptions []sdk.Option client *sdk.SDK + oidcconfig *auth.OIDCConfiguration ) - // If the mode is not all or core, we need to have a valid SDK config - if !slices.Contains(cfg.Mode, "all") && !slices.Contains(cfg.Mode, "core") && cfg.SDKConfig == (config.SDKConfig{}) { - logger.Error("mode is not all or core, but no sdk config provided") - return errors.New("mode is not all or core, but no sdk config provided") + // If the mode is not all or entityresolution, 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") } // If client credentials are provided, use them if cfg.SDKConfig.ClientID != "" && cfg.SDKConfig.ClientSecret != "" { sdkOptions = append(sdkOptions, sdk.WithClientCredentials(cfg.SDKConfig.ClientID, cfg.SDKConfig.ClientSecret, nil)) - oidcconfig, err := auth.DiscoverOIDCConfiguration(ctx, cfg.Server.Auth.Issuer, logger) + oidcconfig, err = auth.DiscoverOIDCConfiguration(ctx, cfg.Server.Auth.Issuer, logger) if err != nil { return fmt.Errorf("could not retrieve oidc configuration: %w", err) } @@ -158,11 +170,83 @@ func Start(f ...StartOptions) error { } // If the mode is all, use IPC for the SDK client - if slices.Contains(cfg.Mode, "all") || slices.Contains(cfg.Mode, "core") { + if slices.Contains(cfg.Mode, "all") || //nolint:nestif // Need to handle all config options + slices.Contains(cfg.Mode, "entityresolution") || // ERS does not connect to anything so it can also use IPC mode + slices.Contains(cfg.Mode, "core") { // Use IPC for the SDK client sdkOptions = append(sdkOptions, sdk.WithIPC()) sdkOptions = append(sdkOptions, sdk.WithCustomCoreConnection(otdf.ConnectRPCInProcess.Conn())) + // handle ERS connection for core mode + if slices.Contains(cfg.Mode, "core") { + logger.Info("core mode") + + if cfg.SDKConfig.EntityResolutionConnection.Endpoint == "" { + return errors.New("entityresolution endpoint must be provided in core mode") + } + + ersDialOptions := []grpc.DialOption{} + var tlsConfig *tls.Config + if cfg.SDKConfig.EntityResolutionConnection.Insecure { + tlsConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: true, // #nosec G402 + } + ersDialOptions = append(ersDialOptions, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) + } + if cfg.SDKConfig.EntityResolutionConnection.Plaintext { + tlsConfig = &tls.Config{} + ersDialOptions = append(ersDialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + if cfg.SDKConfig.ClientID != "" && cfg.SDKConfig.ClientSecret != "" { + if oidcconfig.Issuer == "" { + // this should not occur, it will have been set above if this block is entered + return errors.New("cannot add token interceptor: oidcconfig is empty") + } + + rsaKeyPair, err := ocrypto.NewRSAKeyPair(dpopKeySize) + if err != nil { + return fmt.Errorf("could not generate RSA Key: %w", err) + } + ts, err := sdk.NewIDPAccessTokenSource( + oauth.ClientCredentials{ClientID: cfg.SDKConfig.ClientID, ClientAuth: cfg.SDKConfig.ClientSecret}, + oidcconfig.TokenEndpoint, + nil, + &rsaKeyPair, + ) + if err != nil { + return fmt.Errorf("error creating ERS tokensource: %w", err) + } + + interceptor := sdkauth.NewTokenAddingInterceptor(ts, tlsConfig) + + ersDialOptions = append(ersDialOptions, grpc.WithChainUnaryInterceptor(interceptor.AddCredentials)) + } + + parsedURL, err := url.Parse(cfg.SDKConfig.EntityResolutionConnection.Endpoint) + if err != nil { + return fmt.Errorf("cannot parse ers url(%s): %w", cfg.SDKConfig.EntityResolutionConnection.Endpoint, err) + } + // Needed to support buffconn for testing + if parsedURL.Host == "" { + return errors.New("ERS host is empty when parsing") + } + port := parsedURL.Port() + // if port is empty, default to 443. + if port == "" { + port = "443" + } + ersGRPCEndpoint := net.JoinHostPort(parsedURL.Hostname(), port) + + conn, err := grpc.NewClient(ersGRPCEndpoint, ersDialOptions...) + if err != nil { + return fmt.Errorf("could not connect to ERS: %w", err) + } + sdkOptions = append(sdkOptions, sdk.WithCustomEntityResolutionConnection(conn)) + logger.Info("added with custom ers connection for ", "", ersGRPCEndpoint) + } + client, err = sdk.New("", sdkOptions...) if err != nil { logger.Error("issue creating sdk client", slog.String("error", err.Error())) @@ -170,10 +254,13 @@ func Start(f ...StartOptions) error { } } else { // Use the provided SDK config - if cfg.SDKConfig.Plaintext { + if cfg.SDKConfig.CorePlatformConnection.Insecure { + sdkOptions = append(sdkOptions, sdk.WithInsecureSkipVerifyConn()) + } + if cfg.SDKConfig.CorePlatformConnection.Plaintext { sdkOptions = append(sdkOptions, sdk.WithInsecurePlaintextConn()) } - client, err = sdk.New(cfg.SDKConfig.Endpoint, sdkOptions...) + client, err = sdk.New(cfg.SDKConfig.CorePlatformConnection.Endpoint, sdkOptions...) if err != nil { logger.Error("issue creating sdk client", slog.String("error", err.Error())) return fmt.Errorf("issue creating sdk client: %w", err) diff --git a/test/start-additional-kas/action.yaml b/test/start-additional-kas/action.yaml index d87f4058f..722ac9bbb 100644 --- a/test/start-additional-kas/action.yaml +++ b/test/start-additional-kas/action.yaml @@ -23,7 +23,7 @@ runs: opentdf-${{ inputs.kas-name }}.yaml yq e ' (.server.port = ${{ inputs.kas-port }}) | (.mode = ["kas"]) - | (.sdk_config = {"endpoint":"http://localhost:8080","plaintext":true,"client_id":"opentdf","client_secret":"secret"}) + | (.sdk_config = {"client_id":"opentdf","client_secret":"secret","core":{"endpoint":"http://localhost:8080","plaintext":true}}) ' && .github/scripts/watch.sh opentdf-${{ inputs.kas-name }}.yaml ./opentdf --config-file ./opentdf-${{ inputs.kas-name }}.yaml start wait-on: |