Skip to content

Commit 4114d53

Browse files
Refactor the CyberArk identity client to take an HTTP client
So that we can more easily pass in an HTTP client which is configured with CA certificates to connect to an httptest TLS server. Signed-off-by: Richard Wall <[email protected]>
1 parent 57d3435 commit 4114d53

File tree

7 files changed

+81
-106
lines changed

7 files changed

+81
-106
lines changed

pkg/internal/cyberark/dataupload/dataupload_test.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ import (
99
"testing"
1010
"time"
1111

12+
"github.com/jetstack/venafi-connection-lib/http_client"
1213
"github.com/stretchr/testify/require"
14+
"k8s.io/client-go/transport"
1315
"k8s.io/klog/v2"
1416
"k8s.io/klog/v2/ktesting"
1517

1618
"github.com/jetstack/preflight/api"
1719
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
1820
"github.com/jetstack/preflight/pkg/internal/cyberark/identity"
1921
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
22+
"github.com/jetstack/preflight/pkg/version"
2023

2124
_ "k8s.io/klog/v2/ktesting/init"
2225
)
@@ -129,6 +132,9 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
129132
// ARK_SUBDOMAIN should be your tenant subdomain.
130133
// ARK_PLATFORM_DOMAIN should be either integration-cyberark.cloud or cyberark.cloud
131134
//
135+
// To test against a tenant on the integration platform, also set:
136+
// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2
137+
//
132138
// To enable verbose request logging:
133139
//
134140
// go test ./pkg/internal/cyberark/dataupload/... \
@@ -138,6 +144,10 @@ func TestPostDataReadingsWithOptionsWithRealAPI(t *testing.T) {
138144
subdomain := os.Getenv("ARK_SUBDOMAIN")
139145
username := os.Getenv("ARK_USERNAME")
140146
secret := os.Getenv("ARK_SECRET")
147+
serviceDiscoveryAPI := os.Getenv("ARK_DISCOVERY_API")
148+
if serviceDiscoveryAPI == "" {
149+
serviceDiscoveryAPI = servicediscovery.ProdDiscoveryEndpoint
150+
}
141151

142152
if platformDomain == "" || subdomain == "" || username == "" || secret == "" {
143153
t.Skip("Skipping because one of the following environment variables is unset or empty: ARK_PLATFORM_DOMAIN, ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET")
@@ -154,18 +164,19 @@ func TestPostDataReadingsWithOptionsWithRealAPI(t *testing.T) {
154164

155165
serviceURL := fmt.Sprintf("https://%s%s%s.%s", subdomain, separator, discoveryContextServiceName, platformDomain)
156166

157-
var (
158-
identityClient *identity.Client
159-
err error
167+
var rootCAs *x509.CertPool
168+
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
169+
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
170+
171+
discoveryClient := servicediscovery.New(
172+
servicediscovery.WithHTTPClient(httpClient),
173+
servicediscovery.WithCustomEndpoint(serviceDiscoveryAPI),
160174
)
161-
if platformDomain == "cyberark.cloud" {
162-
identityClient, err = identity.New(ctx, subdomain)
163-
} else {
164-
discoveryClient := servicediscovery.New(servicediscovery.WithIntegrationEndpoint())
165-
identityClient, err = identity.NewWithDiscoveryClient(ctx, discoveryClient, subdomain)
166-
}
175+
176+
identityAPI, err := discoveryClient.DiscoverIdentityAPIURL(ctx, subdomain)
167177
require.NoError(t, err)
168178

179+
identityClient := identity.New(httpClient, identityAPI, subdomain)
169180
err = identityClient.LoginUsernamePassword(ctx, username, []byte(secret))
170181
require.NoError(t, err)
171182

pkg/internal/cyberark/identity/advance_authentication_test.go

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,21 +99,11 @@ func Test_IdentityAdvanceAuthentication(t *testing.T) {
9999
t.Run(name, func(t *testing.T) {
100100
ctx := t.Context()
101101

102-
identityServer := MockIdentityServer()
103-
defer identityServer.Close()
102+
identityAPI, httpClient := MockIdentityServer(t)
104103

105-
mockDiscoveryServer := servicediscovery.MockDiscoveryServerWithCustomAPIURL(identityServer.Server.URL)
106-
defer mockDiscoveryServer.Close()
104+
client := New(httpClient, identityAPI, servicediscovery.MockDiscoverySubdomain)
107105

108-
discoveryClient := servicediscovery.New(servicediscovery.WithCustomEndpoint(mockDiscoveryServer.Server.URL))
109-
110-
client, err := NewWithDiscoveryClient(ctx, discoveryClient, servicediscovery.MockDiscoverySubdomain)
111-
if err != nil {
112-
t.Errorf("failed to create identity client: %s", err)
113-
return
114-
}
115-
116-
err = client.doAdvanceAuthentication(ctx, testSpec.username, &testSpec.password, testSpec.advanceBody)
106+
err := client.doAdvanceAuthentication(ctx, testSpec.username, &testSpec.password, testSpec.advanceBody)
117107
if testSpec.expectedError != err {
118108
if testSpec.expectedError == nil {
119109
t.Errorf("didn't expect an error but got %v", err)

pkg/internal/cyberark/identity/cmd/testidentity/main.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,32 @@ package main
22

33
import (
44
"context"
5+
"crypto/x509"
56
"flag"
67
"fmt"
78
"os"
89
"os/signal"
910

11+
"github.com/jetstack/venafi-connection-lib/http_client"
12+
"k8s.io/client-go/transport"
1013
"k8s.io/klog/v2"
1114

1215
"github.com/jetstack/preflight/pkg/internal/cyberark/identity"
1316
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
17+
"github.com/jetstack/preflight/pkg/version"
1418
)
1519

1620
// This is a trivial CLI application for testing our identity client end-to-end.
1721
// It's not intended for distribution; it simply allows us to run our client and check
1822
// the login is successful.
19-
23+
//
24+
// To test against a tenant on the integration platform, set:
25+
// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2
2026
const (
21-
subdomainFlag = "subdomain"
22-
usernameFlag = "username"
23-
passwordEnv = "TESTIDENTITY_PASSWORD"
27+
subdomainFlag = "subdomain"
28+
usernameFlag = "username"
29+
passwordEnv = "ARK_SECRET"
30+
serviceDiscoveryAPIEnv = "ARK_DISCOVERY_API"
2431
)
2532

2633
var (
@@ -41,16 +48,30 @@ func run(ctx context.Context) error {
4148
if password == "" {
4249
return fmt.Errorf("no password provided in %s", passwordEnv)
4350
}
44-
sdClient := servicediscovery.New(servicediscovery.WithIntegrationEndpoint())
4551

46-
client, err := identity.NewWithDiscoveryClient(ctx, sdClient, subdomain)
52+
serviceDiscoveryAPI := os.Getenv(serviceDiscoveryAPIEnv)
53+
if serviceDiscoveryAPI == "" {
54+
serviceDiscoveryAPI = servicediscovery.ProdDiscoveryEndpoint
55+
}
56+
57+
var rootCAs *x509.CertPool
58+
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
59+
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
60+
61+
sdClient := servicediscovery.New(
62+
servicediscovery.WithHTTPClient(httpClient),
63+
servicediscovery.WithCustomEndpoint(serviceDiscoveryAPI),
64+
)
65+
identityAPI, err := sdClient.DiscoverIdentityAPIURL(ctx, subdomain)
4766
if err != nil {
48-
return err
67+
return fmt.Errorf("while performing service discovery: %s", err)
4968
}
5069

70+
client := identity.New(httpClient, identityAPI, subdomain)
71+
5172
err = client.LoginUsernamePassword(ctx, username, []byte(password))
5273
if err != nil {
53-
return err
74+
return fmt.Errorf("while performing login with username and password: %s", err)
5475
}
5576

5677
return nil
@@ -61,7 +82,7 @@ func main() {
6182

6283
flagSet := flag.NewFlagSet("test", flag.ExitOnError)
6384
klog.InitFlags(flagSet)
64-
_ = flagSet.Parse([]string{"--v", "5"})
85+
_ = flagSet.Parse([]string{"--v", "6"})
6586

6687
logger := klog.Background()
6788

pkg/internal/cyberark/identity/identity.go

Lines changed: 12 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ import (
1212
"time"
1313

1414
"github.com/cenkalti/backoff/v5"
15-
"k8s.io/client-go/transport"
1615
"k8s.io/klog/v2"
1716

18-
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
1917
"github.com/jetstack/preflight/pkg/logs"
2018
"github.com/jetstack/preflight/pkg/version"
2119
)
@@ -177,10 +175,9 @@ type advanceAuthenticationResponseResult struct {
177175
// Client is an client for interacting with the CyberArk Identity API and performing a login using a username and password.
178176
// For context on the behaviour of this client, see the Python SDK: https://github.com/cyberark/ark-sdk-python/blob/3be12c3f2d3a2d0407025028943e584b6edc5996/ark_sdk_python/auth/identity/ark_identity.py
179177
type Client struct {
180-
client *http.Client
181-
182-
endpoint string
183-
subdomain string
178+
httpClient *http.Client
179+
baseURL string
180+
subdomain string
184181

185182
tokenCached token
186183
tokenCachedMutex sync.Mutex
@@ -190,39 +187,15 @@ type Client struct {
190187
type token string
191188

192189
// New returns an initialized CyberArk Identity client using a default service discovery client.
193-
// NB: This function performs service discovery when called, in order to ensure that all Identity
194-
// clients are created with a valid Identity API URL. This function blocks on the network call to
195-
// the discovery service.
196-
func New(ctx context.Context, subdomain string) (*Client, error) {
197-
return NewWithDiscoveryClient(ctx, servicediscovery.New(), subdomain)
198-
}
199-
200-
// NewWithDiscoveryClient returns an initialized CyberArk Identity client using the given service discovery client.
201-
// NB: This function performs service discovery when called, in order to ensure that all Identity
202-
// clients are created with a valid Identity API URL. This function blocks on the network call to
203-
// the discovery service.
204-
func NewWithDiscoveryClient(ctx context.Context, discoveryClient *servicediscovery.Client, subdomain string) (*Client, error) {
205-
if discoveryClient == nil {
206-
return nil, fmt.Errorf("must provide a non-nil discovery client to the Identity Client")
207-
}
208-
209-
endpoint, err := discoveryClient.DiscoverIdentityAPIURL(ctx, subdomain)
210-
if err != nil {
211-
return nil, err
212-
}
213-
190+
func New(httpClient *http.Client, baseURL string, subdomain string) *Client {
214191
return &Client{
215-
client: &http.Client{
216-
Timeout: 10 * time.Second,
217-
Transport: transport.NewDebuggingRoundTripper(http.DefaultTransport, transport.DebugByContext),
218-
},
219-
220-
endpoint: endpoint,
221-
subdomain: subdomain,
192+
httpClient: httpClient,
193+
baseURL: baseURL,
194+
subdomain: subdomain,
222195

223196
tokenCached: "",
224197
tokenCachedMutex: sync.Mutex{},
225-
}, nil
198+
}
226199
}
227200

228201
// LoginUsernamePassword performs a blocking call to fetch an auth token from CyberArk Identity using the given username and password.
@@ -282,7 +255,7 @@ func (c *Client) doStartAuthentication(ctx context.Context, username string) (ad
282255
return response, fmt.Errorf("failed to marshal JSON for request to StartAuthentication endpoint: %s", err)
283256
}
284257

285-
endpoint, err := url.JoinPath(c.endpoint, "Security", "StartAuthentication")
258+
endpoint, err := url.JoinPath(c.baseURL, "Security", "StartAuthentication")
286259
if err != nil {
287260
return response, fmt.Errorf("failed to create URL for request to CyberArk Identity StartAuthentication: %s", err)
288261
}
@@ -294,7 +267,7 @@ func (c *Client) doStartAuthentication(ctx context.Context, username string) (ad
294267

295268
setIdentityHeaders(request)
296269

297-
httpResponse, err := c.client.Do(request)
270+
httpResponse, err := c.httpClient.Do(request)
298271
if err != nil {
299272
return response, fmt.Errorf("failed to perform HTTP request to start authentication: %s", err)
300273
}
@@ -391,7 +364,7 @@ func (c *Client) doAdvanceAuthentication(ctx context.Context, username string, p
391364
return backoff.Permanent(fmt.Errorf("failed to marshal JSON for request to AdvanceAuthentication endpoint: %s", err))
392365
}
393366

394-
endpoint, err := url.JoinPath(c.endpoint, "Security", "AdvanceAuthentication")
367+
endpoint, err := url.JoinPath(c.baseURL, "Security", "AdvanceAuthentication")
395368
if err != nil {
396369
return backoff.Permanent(fmt.Errorf("failed to create URL for request to CyberArk Identity AdvanceAuthentication: %s", err))
397370
}
@@ -403,7 +376,7 @@ func (c *Client) doAdvanceAuthentication(ctx context.Context, username string, p
403376

404377
setIdentityHeaders(request)
405378

406-
httpResponse, err := c.client.Do(request)
379+
httpResponse, err := c.httpClient.Do(request)
407380
if err != nil {
408381
return fmt.Errorf("failed to perform HTTP request to advance authentication: %s", err)
409382
}

pkg/internal/cyberark/identity/mock.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"fmt"
77
"net/http"
88
"net/http/httptest"
9+
"testing"
10+
11+
"k8s.io/client-go/transport"
912

1013
"github.com/jetstack/preflight/pkg/version"
1114

@@ -51,22 +54,17 @@ var (
5154
advanceAuthenticationFailureResponse string
5255
)
5356

54-
type mockIdentityServer struct {
55-
Server *httptest.Server
56-
}
57+
type mockIdentityServer struct{}
5758

58-
// MockIdentityServer returns a mocked CyberArk Identity server.
59-
// The returned server should be Closed by the caller after use.
60-
func MockIdentityServer() *mockIdentityServer {
59+
// MockIdentityServer returns a URL of a mocked CyberArk identity server and an
60+
// HTTP client with the CA certs needed to connect to it..
61+
func MockIdentityServer(t *testing.T) (string, *http.Client) {
6162
mis := &mockIdentityServer{}
62-
63-
mis.Server = httptest.NewServer(mis)
64-
65-
return mis
66-
}
67-
68-
func (mis *mockIdentityServer) Close() {
69-
mis.Server.Close()
63+
server := httptest.NewTLSServer(mis)
64+
t.Cleanup(server.Close)
65+
httpClient := server.Client()
66+
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
67+
return server.URL, httpClient
7068
}
7169

7270
func (mis *mockIdentityServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {

pkg/internal/cyberark/identity/start_authentication_test.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,9 @@ func Test_IdentityStartAuthentication(t *testing.T) {
4040
t.Run(name, func(t *testing.T) {
4141
ctx := t.Context()
4242

43-
identityServer := MockIdentityServer()
44-
defer identityServer.Close()
43+
identityServer, httpClient := MockIdentityServer(t)
4544

46-
mockDiscoveryServer := servicediscovery.MockDiscoveryServerWithCustomAPIURL(identityServer.Server.URL)
47-
defer mockDiscoveryServer.Close()
48-
49-
discoveryClient := servicediscovery.New(servicediscovery.WithCustomEndpoint(mockDiscoveryServer.Server.URL))
50-
51-
client, err := NewWithDiscoveryClient(ctx, discoveryClient, servicediscovery.MockDiscoverySubdomain)
52-
if err != nil {
53-
t.Errorf("failed to create identity client: %s", err)
54-
return
55-
}
45+
client := New(httpClient, identityServer, servicediscovery.MockDiscoverySubdomain)
5646

5747
advanceBody, err := client.doStartAuthentication(ctx, testSpec.username)
5848
if err != nil {

pkg/internal/cyberark/servicediscovery/discovery.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import (
1515
)
1616

1717
const (
18-
prodDiscoveryEndpoint = "https://platform-discovery.cyberark.cloud/api/v2/"
19-
integrationDiscoveryEndpoint = "https://platform-discovery.integration-cyberark.cloud/api/v2/"
18+
ProdDiscoveryEndpoint = "https://platform-discovery.cyberark.cloud/api/v2/"
2019

2120
// identityServiceName is the name of the identity service we're looking for in responses from the Service Discovery API
2221
// We were told to use the identity_administration field, not the identity_user_portal field.
@@ -45,13 +44,6 @@ func WithHTTPClient(httpClient *http.Client) ClientOpt {
4544
}
4645
}
4746

48-
// WithIntegrationEndpoint sets the discovery client to use the integration testing endpoint rather than production
49-
func WithIntegrationEndpoint() ClientOpt {
50-
return func(c *Client) {
51-
c.endpoint = integrationDiscoveryEndpoint
52-
}
53-
}
54-
5547
// WithCustomEndpoint sets the endpoint to a custom URL without checking that the URL is a CyberArk Service Discovery
5648
// server.
5749
func WithCustomEndpoint(endpoint string) ClientOpt {
@@ -67,7 +59,7 @@ func New(clientOpts ...ClientOpt) *Client {
6759
Timeout: 10 * time.Second,
6860
Transport: transport.NewDebuggingRoundTripper(http.DefaultTransport, transport.DebugByContext),
6961
},
70-
endpoint: prodDiscoveryEndpoint,
62+
endpoint: ProdDiscoveryEndpoint,
7163
}
7264

7365
for _, opt := range clientOpts {

0 commit comments

Comments
 (0)