Skip to content

Commit b7adba8

Browse files
CyberArk(agent): add support for MachineHub output mode
- Introduced a new `MachineHub` output mode in the agent configuration - Hooked up the `--machine-hub` flag to enable MachineHub mode - Implemented `CyberArkClient` for interacting with CyberArk APIs - Created `LoadClientConfigFromEnvironment` to load credentials from environment variables - Updated `dataupload` package to use `ClusterID` instead of `ClusterName` - Added unit tests for MachineHub mode and CyberArk client functionality - Updated mock servers and test utilities to support CyberArk integration
1 parent b82d7ce commit b7adba8

File tree

12 files changed

+269
-34
lines changed

12 files changed

+269
-34
lines changed

pkg/agent/config.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package agent
22

33
import (
4+
"crypto/x509"
45
"fmt"
56
"io"
67
"net/url"
@@ -10,9 +11,11 @@ import (
1011

1112
"github.com/go-logr/logr"
1213
"github.com/hashicorp/go-multierror"
14+
"github.com/jetstack/venafi-connection-lib/http_client"
1315
"github.com/spf13/cobra"
1416
"gopkg.in/yaml.v3"
1517
"k8s.io/client-go/rest"
18+
"k8s.io/client-go/transport"
1619

1720
"github.com/jetstack/preflight/api"
1821
"github.com/jetstack/preflight/pkg/client"
@@ -334,6 +337,7 @@ const (
334337
VenafiCloudKeypair OutputMode = "Venafi Cloud Key Pair Service Account"
335338
VenafiCloudVenafiConnection OutputMode = "Venafi Cloud VenafiConnection"
336339
LocalFile OutputMode = "Local File"
340+
MachineHub OutputMode = "MachineHub"
337341
)
338342

339343
// 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)
420424
case !flags.VenafiCloudMode && flags.CredentialsPath != "":
421425
mode = JetstackSecureOAuth
422426
reason = "--credentials-file was specified without --venafi-cloud"
427+
case flags.MachineHubMode:
428+
mode = MachineHub
429+
reason = "--machine-hub was specified"
423430
case flags.OutputPath != "":
424431
mode = LocalFile
425432
reason = "--output-path was specified"
@@ -433,6 +440,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
433440
" - Use --venafi-connection for the " + string(VenafiCloudVenafiConnection) + " mode.\n" +
434441
" - Use --credentials-file alone if you want to use the " + string(JetstackSecureOAuth) + " mode.\n" +
435442
" - Use --api-token if you want to use the " + string(JetstackSecureAPIToken) + " mode.\n" +
443+
" - Use --machine-hub if you want to use the " + string(MachineHub) + " mode.\n" +
436444
" - Use --output-path or output-path in the config file for " + string(LocalFile) + " mode.")
437445
}
438446

@@ -762,6 +770,17 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
762770
}
763771
case LocalFile:
764772
outputClient = client.NewFileClient(cfg.OutputPath)
773+
case MachineHub:
774+
var (
775+
err error
776+
rootCAs *x509.CertPool
777+
)
778+
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
779+
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
780+
outputClient, err = client.NewCyberArk(httpClient)
781+
if err != nil {
782+
errs = multierror.Append(errs, err)
783+
}
765784
default:
766785
panic(fmt.Errorf("programmer mistake: output mode not implemented: %s", cfg.OutputMode))
767786
}

pkg/agent/config_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) {
199199
- Use --venafi-connection for the Venafi Cloud VenafiConnection mode.
200200
- Use --credentials-file alone if you want to use the Jetstack Secure OAuth mode.
201201
- Use --api-token if you want to use the Jetstack Secure API Token mode.
202+
- Use --machine-hub if you want to use the MachineHub mode.
202203
- Use --output-path or output-path in the config file for Local File mode.`))
203204
assert.Nil(t, cl)
204205
})
@@ -617,6 +618,38 @@ func Test_ValidateAndCombineConfig(t *testing.T) {
617618
assert.Equal(t, VenafiCloudVenafiConnection, got.OutputMode)
618619
})
619620

621+
t.Run("--machine-hub selects MachineHub mode", func(t *testing.T) {
622+
t.Setenv("POD_NAMESPACE", "venafi")
623+
t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig))
624+
t.Setenv("ARK_SUBDOMAIN", "tlspk")
625+
t.Setenv("ARK_USERNAME", "[email protected]")
626+
t.Setenv("ARK_SECRET", "test-secret")
627+
got, cl, err := ValidateAndCombineConfig(discardLogs(),
628+
withConfig(""),
629+
withCmdLineFlags("--period", "1m", "--machine-hub"))
630+
require.NoError(t, err)
631+
assert.Equal(t, MachineHub, got.OutputMode)
632+
assert.IsType(t, &client.CyberArkClient{}, cl)
633+
})
634+
635+
t.Run("--machine-hub without required environment variables", func(t *testing.T) {
636+
t.Setenv("POD_NAMESPACE", "venafi")
637+
t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig))
638+
t.Setenv("ARK_SUBDOMAIN", "")
639+
t.Setenv("ARK_USERNAME", "")
640+
t.Setenv("ARK_SECRET", "")
641+
got, cl, err := ValidateAndCombineConfig(discardLogs(),
642+
withConfig(""),
643+
withCmdLineFlags("--period", "1m", "--machine-hub"))
644+
assert.Equal(t, CombinedConfig{}, got)
645+
assert.Nil(t, cl)
646+
assert.EqualError(t, err, testutil.Undent(`
647+
validating creds: failed loading config using the MachineHub mode: 1 error occurred:
648+
* missing environment variables: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET
649+
650+
`))
651+
})
652+
620653
t.Run("argument: --output-file selects local file mode", func(t *testing.T) {
621654
log, gotLog := recordLogs(t)
622655
got, outputClient, err := ValidateAndCombineConfig(log,

pkg/client/client_cyberark.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,51 @@
11
package client
22

33
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/jetstack/preflight/api"
9+
"github.com/jetstack/preflight/pkg/internal/cyberark"
410
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
511
)
612

7-
type CyberArkClient = dataupload.CyberArkClient
13+
type CyberArkClient struct {
14+
configLoader cyberark.ClientConfigLoader
15+
httpClient *http.Client
16+
}
17+
18+
var _ Client = &CyberArkClient{}
19+
20+
func NewCyberArk(httpClient *http.Client) (*CyberArkClient, error) {
21+
configLoader := cyberark.LoadClientConfigFromEnvironment
22+
_, err := configLoader()
23+
if err != nil {
24+
return nil, err
25+
}
26+
return &CyberArkClient{
27+
configLoader: configLoader,
28+
httpClient: httpClient,
29+
}, nil
30+
}
31+
32+
// An API token is obtained by authenticating with the ARK_USERNAME and ARK_SECRET from the environment.
33+
// ARK_SUBDOMAIN should be your tenant subdomain.
34+
func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, options Options) error {
35+
cfg, err := o.configLoader()
36+
if err != nil {
37+
return err
38+
}
39+
datauploadClient, err := cyberark.NewDatauploadClient(ctx, o.httpClient, cfg)
40+
if err != nil {
41+
return fmt.Errorf("while initializing data upload client: %s", err)
42+
}
843

9-
var NewCyberArkClient = dataupload.New
44+
err = datauploadClient.PutSnapshot(ctx, api.DataReadingsPost{}, dataupload.Options{
45+
ClusterID: options.ClusterID,
46+
})
47+
if err != nil {
48+
return fmt.Errorf("while posting snapshot: %s", err)
49+
}
50+
return nil
51+
}

pkg/client/client_cyberark_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package client_test
2+
3+
import (
4+
"net/http"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
"k8s.io/client-go/transport"
10+
"k8s.io/klog/v2"
11+
"k8s.io/klog/v2/ktesting"
12+
13+
"github.com/jetstack/preflight/api"
14+
"github.com/jetstack/preflight/pkg/client"
15+
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
16+
"github.com/jetstack/preflight/pkg/testutil"
17+
18+
_ "k8s.io/klog/v2/ktesting/init"
19+
)
20+
21+
func TestCyberArkClient_PostDataReadingsWithOptions_MockAPI(t *testing.T) {
22+
t.Setenv("ARK_SUBDOMAIN", servicediscovery.MockDiscoverySubdomain)
23+
t.Setenv("ARK_USERNAME", "[email protected]")
24+
t.Setenv("ARK_SECRET", "somepassword")
25+
t.Run("success", func(t *testing.T) {
26+
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
27+
ctx := klog.NewContext(t.Context(), logger)
28+
29+
httpClient := testutil.FakeCyberArk(t)
30+
31+
c, err := client.NewCyberArk(httpClient)
32+
require.NoError(t, err)
33+
34+
var readings []*api.DataReading
35+
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{
36+
ClusterID: "success-cluster-id",
37+
})
38+
require.NoError(t, err)
39+
})
40+
}
41+
42+
// TestCyberArkClient_PostDataReadingsWithOptions_RealAPI demonstrates that the
43+
// dataupload code works with the real inventory API.
44+
//
45+
// To enable verbose request logging:
46+
//
47+
// go test ./pkg/internal/cyberark/dataupload/... \
48+
// -v -count 1 -run TestCyberArkClient_PostDataReadingsWithOptions_RealAPI -args -testing.v 6
49+
func TestCyberArkClient_PostDataReadingsWithOptions_RealAPI(t *testing.T) {
50+
t.Run("success", func(t *testing.T) {
51+
subdomain := os.Getenv("ARK_SUBDOMAIN")
52+
username := os.Getenv("ARK_USERNAME")
53+
secret := os.Getenv("ARK_SECRET")
54+
55+
if subdomain == "" || username == "" || secret == "" {
56+
t.Skip("Skipping because one of the following environment variables is unset or empty: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET")
57+
return
58+
}
59+
60+
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
61+
ctx := klog.NewContext(t.Context(), logger)
62+
63+
c, err := client.NewCyberArk(&http.Client{
64+
Transport: transport.NewDebuggingRoundTripper(http.DefaultTransport, transport.DebugByContext),
65+
})
66+
require.NoError(t, err)
67+
var readings []*api.DataReading
68+
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{
69+
ClusterID: "success-cluster-id",
70+
})
71+
require.NoError(t, err)
72+
})
73+
}

pkg/internal/cyberark/client.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package cyberark
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
"os"
8+
9+
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
10+
"github.com/jetstack/preflight/pkg/internal/cyberark/identity"
11+
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
12+
)
13+
14+
type ClientConfig struct {
15+
subdomain string
16+
username string
17+
secret string
18+
}
19+
20+
type ClientConfigLoader func() (ClientConfig, error)
21+
22+
func LoadClientConfigFromEnvironment() (ClientConfig, error) {
23+
subdomain := os.Getenv("ARK_SUBDOMAIN")
24+
username := os.Getenv("ARK_USERNAME")
25+
secret := os.Getenv("ARK_SECRET")
26+
27+
if subdomain == "" || username == "" || secret == "" {
28+
return ClientConfig{}, errors.New(
29+
"missing environment variables: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET")
30+
}
31+
32+
return ClientConfig{
33+
subdomain: subdomain,
34+
username: username,
35+
secret: secret,
36+
}, nil
37+
38+
}
39+
40+
func NewDatauploadClient(ctx context.Context, httpClient *http.Client, cfg ClientConfig) (*dataupload.CyberArkClient, error) {
41+
discoveryClient := servicediscovery.New(httpClient)
42+
serviceMap, err := discoveryClient.DiscoverServices(ctx, cfg.subdomain)
43+
if err != nil {
44+
return nil, err
45+
}
46+
identityAPI := serviceMap.Identity.API
47+
if identityAPI == "" {
48+
return nil, errors.New("service discovery returned an empty identity API")
49+
}
50+
identityClient := identity.New(httpClient, identityAPI, cfg.subdomain)
51+
err = identityClient.LoginUsernamePassword(ctx, cfg.username, []byte(cfg.secret))
52+
if err != nil {
53+
return nil, err
54+
}
55+
discoveryAPI := serviceMap.DiscoveryContext.API
56+
if discoveryAPI == "" {
57+
return nil, errors.New("service discovery returned an empty discovery API")
58+
}
59+
return dataupload.New(httpClient, discoveryAPI, identityClient.AuthenticateRequest), nil
60+
}

pkg/internal/cyberark/dataupload/dataupload.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type CyberArkClient struct {
3535
}
3636

3737
type Options struct {
38-
ClusterName string
38+
ClusterID string
3939
}
4040

4141
func New(httpClient *http.Client, baseURL string, authenticateRequest func(req *http.Request) error) *CyberArkClient {
@@ -46,7 +46,7 @@ func New(httpClient *http.Client, baseURL string, authenticateRequest func(req *
4646
}
4747
}
4848

49-
// PostDataReadingsWithOptions PUTs the supplied payload to an [AWS presigned URL] which it obtains via the CyberArk inventory API.
49+
// PutSnapshot PUTs the supplied payload to an [AWS presigned URL] which it obtains via the CyberArk inventory API.
5050
// [AWS presigned URL]: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
5151
//
5252
// A SHA256 checksum header is included in the request, to verify that the payload
@@ -60,11 +60,7 @@ func New(httpClient *http.Client, baseURL string, authenticateRequest func(req *
6060
// If you omit that header, it is possible to PUT any data.
6161
// There is a work around listed in that issue which we have shared with the
6262
// CyberArk API team.
63-
func (c *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, payload api.DataReadingsPost, opts Options) error {
64-
if opts.ClusterName == "" {
65-
return fmt.Errorf("programmer mistake: the cluster name (aka `cluster_id` in the config file) cannot be left empty")
66-
}
67-
63+
func (c *CyberArkClient) PutSnapshot(ctx context.Context, payload api.DataReadingsPost, opts Options) error {
6864
encodedBody := &bytes.Buffer{}
6965
hash := sha256.New()
7066
if err := json.NewEncoder(io.MultiWriter(encodedBody, hash)).Encode(payload); err != nil {
@@ -114,7 +110,7 @@ func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksu
114110
Checksum string `json:"checksum_sha256"`
115111
AgentVersion string `json:"agent_version"`
116112
}{
117-
ClusterID: opts.ClusterName,
113+
ClusterID: opts.ClusterID,
118114
Checksum: checksum,
119115
AgentVersion: version.PreflightVersion,
120116
}

0 commit comments

Comments
 (0)