From 5a2ae3cc5778b9010d5f2e68c7160e5c8cc7a1ba Mon Sep 17 00:00:00 2001 From: Richard Wall Date: Thu, 28 Aug 2025 15:59:38 +0100 Subject: [PATCH] CyberArk(agent): add support for MachineHub output mode - Introduced a new `MachineHub` output mode in the agent configuration. - Added `--machine-hub` flag to enable the `MachineHub` mode. - Implemented `CyberArkClient` for publishing data readings to CyberArk's API. - Created `LoadClientConfigFromEnvironment` to load CyberArk client configuration from environment variables. - Updated tests to cover `MachineHub` mode and CyberArk client functionality. - Modified mock data and discovery logic to support CyberArk integration. Signed-off-by: Richard Wall --- .envrc.template | 21 +++++ .gitignore | 1 + examples/machinehub.yaml | 12 +++ pkg/agent/config.go | 26 ++++++ pkg/agent/config_test.go | 33 ++++++++ pkg/client/client_cyberark.go | 57 ++++++++++++- pkg/client/client_cyberark_test.go | 74 +++++++++++++++++ pkg/internal/cyberark/client.go | 72 +++++++++++++++++ pkg/internal/cyberark/client_test.go | 80 +++++++++++++++++++ .../cyberark/dataupload/dataupload.go | 4 - .../cyberark/dataupload/dataupload_test.go | 51 ------------ pkg/internal/cyberark/identity/mock.go | 2 +- .../advance_authentication_success.json | 2 +- .../cyberark/servicediscovery/discovery.go | 11 ++- .../servicediscovery/discovery_test.go | 2 +- .../cyberark/servicediscovery/mock.go | 2 +- pkg/testutil/envtest.go | 30 +++++++ 17 files changed, 415 insertions(+), 65 deletions(-) create mode 100644 .envrc.template create mode 100644 examples/machinehub.yaml create mode 100644 pkg/client/client_cyberark_test.go create mode 100644 pkg/internal/cyberark/client.go create mode 100644 pkg/internal/cyberark/client_test.go diff --git a/.envrc.template b/.envrc.template new file mode 100644 index 00000000..6765a4d2 --- /dev/null +++ b/.envrc.template @@ -0,0 +1,21 @@ +# Example .envrc file for use with direnv. +# Copy this file to .envrc and edit the values as required. +# Do not check in your .envrc file to source control as it may contain secrets. + +# The following variables are required by the E2E test script: ./hack/e2e/test.sh. +export VEN_API_KEY= # your Venafi Cloud API key with full permissions +export VEN_API_KEY_PULL= # your Venafi Cloud API key with pull-only permissions +export VEN_ZONE= # the Venafi Cloud zone to use for certificate requests +export VEN_VCP_REGION= # the Venafi Cloud region to use (us or eu) +export VEN_API_HOST= # the Venafi Cloud API host (usually api.venafi.cloud or api.venafi.eu) +export OCI_BASE= # the base URL for the OCI registry where the Agent chart and image will be pushed +export CLOUDSDK_CORE_PROJECT= # the GCP project ID where a GKE cluster will be created. +export CLOUDSDK_COMPUTE_ZONE= # the GCP zone where a GKE cluster will be created. E.g. europe-west2-b +export CLUSTER_NAME= # the name of the GKE cluster which will be created. E.g. cluster-1 + +# The following variables are required for CyberArk / MachineHub integration tests. +export ARK_SUBDOMAIN= # your CyberArk tenant subdomain +export ARK_USERNAME= # your CyberArk username +export ARK_SECRET= # your CyberArk password +# OPTIONAL: the URL for the CyberArk Discovery API if not using the production environment +export ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2 diff --git a/.gitignore b/.gitignore index 17966351..ec041a7c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ predicate.json *.tgz _bin +.envrc diff --git a/examples/machinehub.yaml b/examples/machinehub.yaml new file mode 100644 index 00000000..94b19d48 --- /dev/null +++ b/examples/machinehub.yaml @@ -0,0 +1,12 @@ +# An example agent config for MachineHub output mode. +# +# For example: +# +# export ARK_SUBDOMAIN= # your CyberArk tenant subdomain +# export ARK_USERNAME= # your CyberArk username +# export ARK_SECRET= # your CyberArk password +# go run . agent --one-shot --machine-hub -v 6 --agent-config-file ./examples/machinehub.yaml + +data-gatherers: + - kind: "dummy" + name: "dummy" diff --git a/pkg/agent/config.go b/pkg/agent/config.go index 2f691938..34967e19 100644 --- a/pkg/agent/config.go +++ b/pkg/agent/config.go @@ -1,6 +1,7 @@ package agent import ( + "crypto/x509" "fmt" "io" "net/url" @@ -10,9 +11,11 @@ import ( "github.com/go-logr/logr" "github.com/hashicorp/go-multierror" + "github.com/jetstack/venafi-connection-lib/http_client" "github.com/spf13/cobra" "gopkg.in/yaml.v3" "k8s.io/client-go/rest" + "k8s.io/client-go/transport" "github.com/jetstack/preflight/api" "github.com/jetstack/preflight/pkg/client" @@ -334,6 +337,7 @@ const ( VenafiCloudKeypair OutputMode = "Venafi Cloud Key Pair Service Account" VenafiCloudVenafiConnection OutputMode = "Venafi Cloud VenafiConnection" LocalFile OutputMode = "Local File" + MachineHub OutputMode = "MachineHub" ) // The command-line flags and the config file are combined into this struct by @@ -420,6 +424,9 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) case !flags.VenafiCloudMode && flags.CredentialsPath != "": mode = JetstackSecureOAuth reason = "--credentials-file was specified without --venafi-cloud" + case flags.MachineHubMode: + mode = MachineHub + reason = "--machine-hub was specified" case flags.OutputPath != "": mode = LocalFile reason = "--output-path was specified" @@ -433,6 +440,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) " - Use --venafi-connection for the " + string(VenafiCloudVenafiConnection) + " mode.\n" + " - Use --credentials-file alone if you want to use the " + string(JetstackSecureOAuth) + " mode.\n" + " - Use --api-token if you want to use the " + string(JetstackSecureAPIToken) + " mode.\n" + + " - Use --machine-hub if you want to use the " + string(MachineHub) + " mode.\n" + " - Use --output-path or output-path in the config file for " + string(LocalFile) + " mode.") } @@ -548,6 +556,13 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) } organizationID = cfg.OrganizationID clusterID = cfg.ClusterID + case MachineHub: + if cfg.ClusterID != "" { + log.Info(fmt.Sprintf(`Ignoring the cluster_id field in the config file. This field is not needed in %s mode.`, res.OutputMode)) + } + if cfg.OrganizationID != "" { + log.Info(fmt.Sprintf(`Ignoring the organization_id field in the config file. This field is not needed in %s mode.`, res.OutputMode)) + } } res.OrganizationID = organizationID res.ClusterID = clusterID @@ -762,6 +777,17 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie } case LocalFile: outputClient = client.NewFileClient(cfg.OutputPath) + case MachineHub: + var ( + err error + rootCAs *x509.CertPool + ) + httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs) + httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext) + outputClient, err = client.NewCyberArk(httpClient) + if err != nil { + errs = multierror.Append(errs, err) + } default: panic(fmt.Errorf("programmer mistake: output mode not implemented: %s", cfg.OutputMode)) } diff --git a/pkg/agent/config_test.go b/pkg/agent/config_test.go index fe5211d3..70951b57 100644 --- a/pkg/agent/config_test.go +++ b/pkg/agent/config_test.go @@ -199,6 +199,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) { - Use --venafi-connection for the Venafi Cloud VenafiConnection mode. - Use --credentials-file alone if you want to use the Jetstack Secure OAuth mode. - Use --api-token if you want to use the Jetstack Secure API Token mode. + - Use --machine-hub if you want to use the MachineHub mode. - Use --output-path or output-path in the config file for Local File mode.`)) assert.Nil(t, cl) }) @@ -617,6 +618,38 @@ func Test_ValidateAndCombineConfig(t *testing.T) { assert.Equal(t, VenafiCloudVenafiConnection, got.OutputMode) }) + t.Run("--machine-hub selects MachineHub mode", func(t *testing.T) { + t.Setenv("POD_NAMESPACE", "venafi") + t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig)) + t.Setenv("ARK_SUBDOMAIN", "tlspk") + t.Setenv("ARK_USERNAME", "first_last@cyberark.cloud.123456") + t.Setenv("ARK_SECRET", "test-secret") + got, cl, err := ValidateAndCombineConfig(discardLogs(), + withConfig(""), + withCmdLineFlags("--period", "1m", "--machine-hub")) + require.NoError(t, err) + assert.Equal(t, MachineHub, got.OutputMode) + assert.IsType(t, &client.CyberArkClient{}, cl) + }) + + t.Run("--machine-hub without required environment variables", func(t *testing.T) { + t.Setenv("POD_NAMESPACE", "venafi") + t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig)) + t.Setenv("ARK_SUBDOMAIN", "") + t.Setenv("ARK_USERNAME", "") + t.Setenv("ARK_SECRET", "") + got, cl, err := ValidateAndCombineConfig(discardLogs(), + withConfig(""), + withCmdLineFlags("--period", "1m", "--machine-hub")) + assert.Equal(t, CombinedConfig{}, got) + assert.Nil(t, cl) + assert.EqualError(t, err, testutil.Undent(` + validating creds: failed loading config using the MachineHub mode: 1 error occurred: + * missing environment variables: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET + + `)) + }) + t.Run("argument: --output-file selects local file mode", func(t *testing.T) { log, gotLog := recordLogs(t) got, outputClient, err := ValidateAndCombineConfig(log, diff --git a/pkg/client/client_cyberark.go b/pkg/client/client_cyberark.go index c5800076..c1522a9c 100644 --- a/pkg/client/client_cyberark.go +++ b/pkg/client/client_cyberark.go @@ -1,9 +1,62 @@ package client import ( + "context" + "fmt" + "net/http" + + "github.com/jetstack/preflight/api" + "github.com/jetstack/preflight/pkg/internal/cyberark" "github.com/jetstack/preflight/pkg/internal/cyberark/dataupload" + "github.com/jetstack/preflight/pkg/version" ) -type CyberArkClient = dataupload.CyberArkClient +// CyberArkClient is a client for publishing data readings to CyberArk's discoverycontext API. +type CyberArkClient struct { + configLoader cyberark.ClientConfigLoader + httpClient *http.Client +} + +var _ Client = &CyberArkClient{} + +// NewCyberArk initializes a CyberArk client using configuration from environment variables. +// It requires an HTTP client to be provided, which will be used for making requests. +// The environment variables ARK_SUBDOMAIN, ARK_USERNAME, and ARK_SECRET must be set for authentication. +// If the configuration is invalid or missing, an error is returned. +func NewCyberArk(httpClient *http.Client) (*CyberArkClient, error) { + configLoader := cyberark.LoadClientConfigFromEnvironment + _, err := configLoader() + if err != nil { + return nil, err + } + return &CyberArkClient{ + configLoader: configLoader, + httpClient: httpClient, + }, nil +} + +// PostDataReadingsWithOptions uploads data readings to CyberArk. +// It initializes a data upload client with the configured HTTP client and credentials, +// then uploads a snapshot. +// The supplied Options are not used by this publisher. +func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, _ Options) error { + cfg, err := o.configLoader() + if err != nil { + return err + } + datauploadClient, err := cyberark.NewDatauploadClient(ctx, o.httpClient, cfg) + if err != nil { + return fmt.Errorf("while initializing data upload client: %s", err) + } -var NewCyberArkClient = dataupload.New + err = datauploadClient.PutSnapshot(ctx, dataupload.Snapshot{ + // Temporary hard coded cluster ID. + // TODO(wallrj): The clusterID will eventually be extracted from the supplied readings. + ClusterID: "success-cluster-id", + AgentVersion: version.PreflightVersion, + }) + if err != nil { + return fmt.Errorf("while uploading snapshot: %s", err) + } + return nil +} diff --git a/pkg/client/client_cyberark_test.go b/pkg/client/client_cyberark_test.go new file mode 100644 index 00000000..700e6cca --- /dev/null +++ b/pkg/client/client_cyberark_test.go @@ -0,0 +1,74 @@ +package client_test + +import ( + "crypto/x509" + "errors" + "testing" + + "github.com/jetstack/venafi-connection-lib/http_client" + "github.com/stretchr/testify/require" + "k8s.io/client-go/transport" + "k8s.io/klog/v2" + "k8s.io/klog/v2/ktesting" + + "github.com/jetstack/preflight/api" + "github.com/jetstack/preflight/pkg/client" + "github.com/jetstack/preflight/pkg/internal/cyberark" + "github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery" + "github.com/jetstack/preflight/pkg/testutil" + "github.com/jetstack/preflight/pkg/version" + + _ "k8s.io/klog/v2/ktesting/init" +) + +// TestCyberArkClient_PostDataReadingsWithOptions_MockAPI demonstrates that the +// dataupload code works with the mock CyberArk APIs. +// The environment variables are chosen to match those expected by the mock +// server. +func TestCyberArkClient_PostDataReadingsWithOptions_MockAPI(t *testing.T) { + t.Setenv("ARK_SUBDOMAIN", servicediscovery.MockDiscoverySubdomain) + t.Setenv("ARK_USERNAME", "test@example.com") + t.Setenv("ARK_SECRET", "somepassword") + t.Run("success", func(t *testing.T) { + logger := ktesting.NewLogger(t, ktesting.DefaultConfig) + ctx := klog.NewContext(t.Context(), logger) + + httpClient := testutil.FakeCyberArk(t) + + c, err := client.NewCyberArk(httpClient) + require.NoError(t, err) + + var readings []*api.DataReading + err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{}) + require.NoError(t, err) + }) +} + +// TestCyberArkClient_PostDataReadingsWithOptions_RealAPI demonstrates that the +// dataupload code works with the real CyberArk APIs. +// +// To enable verbose request logging: +// +// go test ./pkg/internal/cyberark/dataupload/... \ +// -v -count 1 -run TestCyberArkClient_PostDataReadingsWithOptions_RealAPI -args -testing.v 6 +func TestCyberArkClient_PostDataReadingsWithOptions_RealAPI(t *testing.T) { + t.Run("success", func(t *testing.T) { + logger := ktesting.NewLogger(t, ktesting.DefaultConfig) + ctx := klog.NewContext(t.Context(), logger) + + var rootCAs *x509.CertPool + httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs) + httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext) + + c, err := client.NewCyberArk(httpClient) + if err != nil { + if errors.Is(err, cyberark.ErrMissingEnvironmentVariables) { + t.Skipf("Skipping: %s", err) + } + require.NoError(t, err) + } + var readings []*api.DataReading + err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{}) + require.NoError(t, err) + }) +} diff --git a/pkg/internal/cyberark/client.go b/pkg/internal/cyberark/client.go new file mode 100644 index 00000000..36d9202b --- /dev/null +++ b/pkg/internal/cyberark/client.go @@ -0,0 +1,72 @@ +package cyberark + +import ( + "context" + "errors" + "net/http" + "os" + + "github.com/jetstack/preflight/pkg/internal/cyberark/dataupload" + "github.com/jetstack/preflight/pkg/internal/cyberark/identity" + "github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery" +) + +// ClientConfig holds the configuration needed to initialize a CyberArk client. +type ClientConfig struct { + Subdomain string + Username string + Secret string +} + +// ClientConfigLoader is a function type that loads and returns a ClientConfig. +type ClientConfigLoader func() (ClientConfig, error) + +// ErrMissingEnvironmentVariables is returned when required environment variables are not set. +var ErrMissingEnvironmentVariables = errors.New("missing environment variables: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET") + +// LoadClientConfigFromEnvironment loads the CyberArk client configuration from environment variables. +// It expects the following environment variables to be set: +// - ARK_SUBDOMAIN: The CyberArk subdomain to use. +// - ARK_USERNAME: The username for authentication. +// - ARK_SECRET: The secret for authentication. +func LoadClientConfigFromEnvironment() (ClientConfig, error) { + subdomain := os.Getenv("ARK_SUBDOMAIN") + username := os.Getenv("ARK_USERNAME") + secret := os.Getenv("ARK_SECRET") + + if subdomain == "" || username == "" || secret == "" { + return ClientConfig{}, ErrMissingEnvironmentVariables + } + + return ClientConfig{ + Subdomain: subdomain, + Username: username, + Secret: secret, + }, nil + +} + +// NewDatauploadClient initializes and returns a new CyberArk Data Upload client. +// It performs service discovery to find the necessary API endpoints and authenticates +// using the provided client configuration. +func NewDatauploadClient(ctx context.Context, httpClient *http.Client, cfg ClientConfig) (*dataupload.CyberArkClient, error) { + discoveryClient := servicediscovery.New(httpClient) + serviceMap, err := discoveryClient.DiscoverServices(ctx, cfg.Subdomain) + if err != nil { + return nil, err + } + identityAPI := serviceMap.Identity.API + if identityAPI == "" { + return nil, errors.New("service discovery returned an empty identity API") + } + identityClient := identity.New(httpClient, identityAPI, cfg.Subdomain) + err = identityClient.LoginUsernamePassword(ctx, cfg.Username, []byte(cfg.Secret)) + if err != nil { + return nil, err + } + discoveryAPI := serviceMap.DiscoveryContext.API + if discoveryAPI == "" { + return nil, errors.New("service discovery returned an empty discovery API") + } + return dataupload.New(httpClient, discoveryAPI, identityClient.AuthenticateRequest), nil +} diff --git a/pkg/internal/cyberark/client_test.go b/pkg/internal/cyberark/client_test.go new file mode 100644 index 00000000..c8fa98ab --- /dev/null +++ b/pkg/internal/cyberark/client_test.go @@ -0,0 +1,80 @@ +package cyberark_test + +import ( + "crypto/x509" + "errors" + "testing" + + "github.com/jetstack/venafi-connection-lib/http_client" + "github.com/stretchr/testify/require" + "k8s.io/client-go/transport" + "k8s.io/klog/v2" + "k8s.io/klog/v2/ktesting" + + "github.com/jetstack/preflight/pkg/internal/cyberark" + "github.com/jetstack/preflight/pkg/internal/cyberark/dataupload" + "github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery" + "github.com/jetstack/preflight/pkg/testutil" + "github.com/jetstack/preflight/pkg/version" + + _ "k8s.io/klog/v2/ktesting/init" +) + +// TestCyberArkClient_PutSnapshot_MockAPI demonstrates that NewDatauploadClient works with the mock API. +func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) { + logger := ktesting.NewLogger(t, ktesting.DefaultConfig) + ctx := klog.NewContext(t.Context(), logger) + + httpClient := testutil.FakeCyberArk(t) + + cfg := cyberark.ClientConfig{ + Subdomain: servicediscovery.MockDiscoverySubdomain, + Username: "test@example.com", + Secret: "somepassword", + } + + cl, err := cyberark.NewDatauploadClient(ctx, httpClient, cfg) + require.NoError(t, err) + + err = cl.PutSnapshot(ctx, dataupload.Snapshot{ + ClusterID: "success-cluster-id", + }) + require.NoError(t, err) +} + +// TestCyberArkClient_PutSnapshot_RealAPI demonstrates that NewDatauploadClient works with the real inventory API. +// +// An API token is obtained by authenticating with the ARK_USERNAME and ARK_SECRET from the environment. +// ARK_SUBDOMAIN should be your tenant subdomain. +// +// To test against a tenant on the integration platform, also set: +// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2 +// +// To enable verbose request logging: +// +// go test ./pkg/internal/cyberark \ +// -v -count 1 -run TestCyberArkClient_PutSnapshot_RealAPI -args -testing.v 6 +func TestCyberArkClient_PutSnapshot_RealAPI(t *testing.T) { + logger := ktesting.NewLogger(t, ktesting.DefaultConfig) + ctx := klog.NewContext(t.Context(), logger) + + var rootCAs *x509.CertPool + httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs) + httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext) + + cfg, err := cyberark.LoadClientConfigFromEnvironment() + if err != nil { + if errors.Is(err, cyberark.ErrMissingEnvironmentVariables) { + t.Skipf("Skipping: %s", err) + } + require.NoError(t, err) + } + + cl, err := cyberark.NewDatauploadClient(ctx, httpClient, cfg) + require.NoError(t, err) + + err = cl.PutSnapshot(ctx, dataupload.Snapshot{ + ClusterID: "bb068932-c80d-460d-88df-34bc7f3f3297", + }) + require.NoError(t, err) +} diff --git a/pkg/internal/cyberark/dataupload/dataupload.go b/pkg/internal/cyberark/dataupload/dataupload.go index 82fdd4b1..9d9444f8 100644 --- a/pkg/internal/cyberark/dataupload/dataupload.go +++ b/pkg/internal/cyberark/dataupload/dataupload.go @@ -35,10 +35,6 @@ type CyberArkClient struct { authenticateRequest func(req *http.Request) error } -type Options struct { - ClusterName string -} - func New(httpClient *http.Client, baseURL string, authenticateRequest func(req *http.Request) error) *CyberArkClient { return &CyberArkClient{ baseURL: baseURL, diff --git a/pkg/internal/cyberark/dataupload/dataupload_test.go b/pkg/internal/cyberark/dataupload/dataupload_test.go index 9df78244..4c278e20 100644 --- a/pkg/internal/cyberark/dataupload/dataupload_test.go +++ b/pkg/internal/cyberark/dataupload/dataupload_test.go @@ -1,22 +1,15 @@ package dataupload_test import ( - "crypto/x509" "fmt" "net/http" - "os" "testing" - "github.com/jetstack/venafi-connection-lib/http_client" "github.com/stretchr/testify/require" - "k8s.io/client-go/transport" "k8s.io/klog/v2" "k8s.io/klog/v2/ktesting" "github.com/jetstack/preflight/pkg/internal/cyberark/dataupload" - "github.com/jetstack/preflight/pkg/internal/cyberark/identity" - "github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery" - "github.com/jetstack/preflight/pkg/version" _ "k8s.io/klog/v2/ktesting/init" ) @@ -109,47 +102,3 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) { }) } } - -// TestCyberArkClient_PutSnapshot_RealAPI demonstrates that the dataupload code works with the real inventory API. -// An API token is obtained by authenticating with the ARK_USERNAME and ARK_SECRET from the environment. -// ARK_SUBDOMAIN should be your tenant subdomain. -// -// To test against a tenant on the integration platform, also set: -// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2 -// -// To enable verbose request logging: -// -// go test ./pkg/internal/cyberark/dataupload/... \ -// -v -count 1 -run TestCyberArkClient_PutSnapshot_RealAPI -args -testing.v 6 -func TestCyberArkClient_PutSnapshot_RealAPI(t *testing.T) { - subdomain := os.Getenv("ARK_SUBDOMAIN") - username := os.Getenv("ARK_USERNAME") - secret := os.Getenv("ARK_SECRET") - - if subdomain == "" || username == "" || secret == "" { - t.Skip("Skipping because one of the following environment variables is unset or empty: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET") - return - } - - logger := ktesting.NewLogger(t, ktesting.DefaultConfig) - ctx := klog.NewContext(t.Context(), logger) - - var rootCAs *x509.CertPool - httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs) - httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext) - - discoveryClient := servicediscovery.New(httpClient) - - services, err := discoveryClient.DiscoverServices(ctx, subdomain) - require.NoError(t, err) - - identityClient := identity.New(httpClient, services.Identity.API, subdomain) - err = identityClient.LoginUsernamePassword(ctx, username, []byte(secret)) - require.NoError(t, err) - - cyberArkClient := dataupload.New(httpClient, services.DiscoveryContext.API, identityClient.AuthenticateRequest) - err = cyberArkClient.PutSnapshot(ctx, dataupload.Snapshot{ - ClusterID: "bb068932-c80d-460d-88df-34bc7f3f3297", - }) - require.NoError(t, err) -} diff --git a/pkg/internal/cyberark/identity/mock.go b/pkg/internal/cyberark/identity/mock.go index 81854f44..d904a695 100644 --- a/pkg/internal/cyberark/identity/mock.go +++ b/pkg/internal/cyberark/identity/mock.go @@ -28,7 +28,7 @@ const ( // mockSuccessfulStartAuthenticationToken is the token returned by the // mock server in response to a successful AdvanceAuthentication request // Must match what's in testdata/advance_authentication_success.json - mockSuccessfulStartAuthenticationToken = "long-token" + mockSuccessfulStartAuthenticationToken = "success-token" ) var ( diff --git a/pkg/internal/cyberark/identity/testdata/advance_authentication_success.json b/pkg/internal/cyberark/identity/testdata/advance_authentication_success.json index c35ed116..62495358 100644 --- a/pkg/internal/cyberark/identity/testdata/advance_authentication_success.json +++ b/pkg/internal/cyberark/identity/testdata/advance_authentication_success.json @@ -3,7 +3,7 @@ "Result": { "AuthLevel": "Normal", "DisplayName": "Namey McNamerson", - "Token": "long-token", + "Token": "success-token", "Auth": "auth-auth", "UserId": "11111111-2222-3333-4444-555555555555", "EmailAddress": "name@example.com", diff --git a/pkg/internal/cyberark/servicediscovery/discovery.go b/pkg/internal/cyberark/servicediscovery/discovery.go index c2cad928..9aeab674 100644 --- a/pkg/internal/cyberark/servicediscovery/discovery.go +++ b/pkg/internal/cyberark/servicediscovery/discovery.go @@ -16,9 +16,13 @@ const ( // ProdDiscoveryAPIBaseURL is the base URL for the production CyberArk Service Discovery API ProdDiscoveryAPIBaseURL = "https://platform-discovery.cyberark.cloud/api/v2/" - // identityServiceName is the name of the identity service we're looking for in responses from the Service Discovery API + // IdentityServiceName is the name of the identity service we're looking for in responses from the Service Discovery API // We were told to use the identity_administration field, not the identity_user_portal field. - identityServiceName = "identity_administration" + IdentityServiceName = "identity_administration" + + // DiscoveryContextServiceName is the name of the discovery and context API + // in responses from the Service Discovery API. + DiscoveryContextServiceName = "discoverycontext" // maxDiscoverBodySize is the maximum allowed size for a response body from the CyberArk Service Discovery subdomain endpoint // As of 2025-04-16, a response from the integration environment is ~4kB @@ -101,7 +105,6 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi } var services Services - err = json.NewDecoder(io.LimitReader(resp.Body, maxDiscoverBodySize)).Decode(&services) if err != nil { if err == io.ErrUnexpectedEOF { @@ -112,7 +115,7 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi } if services.Identity.API == "" { - return nil, fmt.Errorf("didn't find %s in service discovery response, which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", identityServiceName) + return nil, fmt.Errorf("didn't find %s in service discovery response, which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", IdentityServiceName) } return &services, nil diff --git a/pkg/internal/cyberark/servicediscovery/discovery_test.go b/pkg/internal/cyberark/servicediscovery/discovery_test.go index cc1900f2..d1091307 100644 --- a/pkg/internal/cyberark/servicediscovery/discovery_test.go +++ b/pkg/internal/cyberark/servicediscovery/discovery_test.go @@ -31,7 +31,7 @@ func Test_DiscoverIdentityAPIURL(t *testing.T) { "no identity service in response": { subdomain: "no-identity", expectedURL: "", - expectedError: fmt.Errorf("didn't find %s in service discovery response, which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", identityServiceName), + expectedError: fmt.Errorf("didn't find %s in service discovery response, which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", IdentityServiceName), }, "unexpected HTTP response": { subdomain: "bad-request", diff --git a/pkg/internal/cyberark/servicediscovery/mock.go b/pkg/internal/cyberark/servicediscovery/mock.go index 87d8ef36..12607903 100644 --- a/pkg/internal/cyberark/servicediscovery/mock.go +++ b/pkg/internal/cyberark/servicediscovery/mock.go @@ -48,7 +48,7 @@ type mockDiscoveryServer struct { // // The returned HTTP client has a transport which logs requests and responses // depending on log level of the logger supplied in the context. -func MockDiscoveryServer(t *testing.T, services Services) *http.Client { +func MockDiscoveryServer(t testing.TB, services Services) *http.Client { tmpl := template.Must(template.New("mockDiscoverySuccess").Parse(discoverySuccessTemplate)) buf := &bytes.Buffer{} err := tmpl.Execute(buf, services) diff --git a/pkg/testutil/envtest.go b/pkg/testutil/envtest.go index 149368f5..73251ec6 100644 --- a/pkg/testutil/envtest.go +++ b/pkg/testutil/envtest.go @@ -26,6 +26,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" "github.com/jetstack/preflight/pkg/client" + "github.com/jetstack/preflight/pkg/internal/cyberark/dataupload" + "github.com/jetstack/preflight/pkg/internal/cyberark/identity" + "github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery" ) // To see the API server logs, set: @@ -259,6 +262,33 @@ func FakeTPP(t testing.TB) (*httptest.Server, *x509.Certificate) { return server, cert } +// FakeCyberArk returns an HTTP client that will route requests to mock CyberArk +// Service Discovery, Identity and Discovery and Context APIs. This is useful +// for testing code that uses all those APIs, such as +// `cyberark.NewDatauploadClient`. +// +// The environment variable `ARK_DISCOVERY_API` is set to the URL of the mock +// Service Discovery API, for the supplied `testing.TB` so that the client under +// test will use the mock Service Discovery API. +// +// The returned HTTP client has a transport which logs requests and responses +// depending on log level of the logger supplied in the context. +func FakeCyberArk(t testing.TB) *http.Client { + t.Helper() + + identityAPI, _ := identity.MockIdentityServer(t) + discoveryContextAPI, _ := dataupload.MockDataUploadServer(t) + httpClient := servicediscovery.MockDiscoveryServer(t, servicediscovery.Services{ + Identity: servicediscovery.ServiceEndpoint{ + API: identityAPI, + }, + DiscoveryContext: servicediscovery.ServiceEndpoint{ + API: discoveryContextAPI, + }, + }) + return httpClient +} + // Generated using: // // helm template ./deploy/charts/venafi-kubernetes-agent -n venafi --set crds.venafiConnection.include=true --show-only templates/venafi-connection-rbac.yaml | grep -ivE '(helm|\/version)'