Skip to content

Commit b7cf60b

Browse files
authored
Merge pull request #777 from jetstack/keyfetch
Add support for fetching keys from a JWKS endpoint
2 parents d9a7b26 + b4ccb05 commit b7cf60b

23 files changed

Lines changed: 987 additions & 322 deletions

File tree

examples/encrypted-secrets/README.md

Lines changed: 0 additions & 47 deletions
This file was deleted.

examples/encrypted-secrets/config.yaml

Lines changed: 0 additions & 41 deletions
This file was deleted.

examples/encrypted-secrets/test.sh

Lines changed: 0 additions & 65 deletions
This file was deleted.

hack/ark/test-e2e.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ kubectl apply -f "${root_dir}/hack/ark/cluster-external-secret.yaml"
101101

102102
# We use a non-existent tag and omit the `--version` flag, to work around a Helm
103103
# v4 bug. See: https://github.com/helm/helm/issues/31600
104+
# TODO: shouldn't need to set config.sendSecretValues because it will default to true in future
104105
helm upgrade agent "oci://${ARK_CHART}:NON_EXISTENT_TAG@${ARK_CHART_DIGEST}" \
105106
--install \
106107
--wait \
@@ -113,6 +114,7 @@ helm upgrade agent "oci://${ARK_CHART}:NON_EXISTENT_TAG@${ARK_CHART_DIGEST}" \
113114
--set config.clusterName="e2e-test-cluster" \
114115
--set config.clusterDescription="A temporary cluster for E2E testing. Contact @wallrj-cyberark." \
115116
--set config.period=60s \
117+
--set config.sendSecretValues=true \
116118
--set-json "podLabels={\"disco-agent.cyberark.cloud/test-id\": \"${RANDOM}\"}"
117119

118120
kubectl rollout status deployments/disco-agent --namespace "${NAMESPACE}"

internal/cyberark/client_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
3232
Secret: "somepassword",
3333
}
3434

35-
discoveryClient := servicediscovery.New(httpClient)
35+
discoveryClient := servicediscovery.New(httpClient, cfg.Subdomain)
3636

37-
serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(t.Context(), cfg.Subdomain)
37+
serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(t.Context())
3838
if err != nil {
3939
t.Fatalf("failed to discover mock services: %v", err)
4040
}
@@ -76,9 +76,9 @@ func TestCyberArkClient_PutSnapshot_RealAPI(t *testing.T) {
7676
cfg, err := cyberark.LoadClientConfigFromEnvironment()
7777
require.NoError(t, err)
7878

79-
discoveryClient := servicediscovery.New(httpClient)
79+
discoveryClient := servicediscovery.New(httpClient, cfg.Subdomain)
8080

81-
serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(t.Context(), cfg.Subdomain)
81+
serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(t.Context())
8282
if err != nil {
8383
t.Fatalf("failed to discover services: %v", err)
8484
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ func run(ctx context.Context) error {
5050
var rootCAs *x509.CertPool
5151
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
5252

53-
sdClient := servicediscovery.New(httpClient)
54-
services, _, err := sdClient.DiscoverServices(ctx, subdomain)
53+
sdClient := servicediscovery.New(httpClient, subdomain)
54+
services, _, err := sdClient.DiscoverServices(ctx)
5555
if err != nil {
5656
return fmt.Errorf("while performing service discovery: %s", err)
5757
}

internal/cyberark/identity/identity.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"net/url"
1111
"sync"
12+
"time"
1213

1314
"k8s.io/klog/v2"
1415

@@ -180,6 +181,7 @@ type Client struct {
180181

181182
tokenCached token
182183
tokenCachedMutex sync.Mutex
184+
tokenCachedTime time.Time
183185
}
184186

185187
// token is a wrapper type for holding auth tokens we want to cache.
@@ -205,12 +207,23 @@ func New(httpClient *http.Client, baseURL string, subdomain string) *Client {
205207
// Tokens are cached internally and are not directly accessible to code; use Client.AuthenticateRequest to add credentials
206208
// to an *http.Request.
207209
func (c *Client) LoginUsernamePassword(ctx context.Context, username string, password []byte) error {
210+
// note: we hold the mutex for the whole login attempt to ensure that only one login attempt can be in flight at once,
211+
// and to ensure that the token cache is correctly updated
212+
c.tokenCachedMutex.Lock()
213+
defer c.tokenCachedMutex.Unlock()
214+
208215
defer func() {
209216
for i := range password {
210217
password[i] = 0x00
211218
}
212219
}()
213220

221+
if time.Since(c.tokenCachedTime) < 15*time.Minute && c.tokenCached.Username == username {
222+
// If the cached token is recent and for the same username, we can reuse it.
223+
klog.FromContext(ctx).V(2).Info("reusing cached token for user", "username", username)
224+
return nil
225+
}
226+
214227
advanceRequestBody, err := c.doStartAuthentication(ctx, username)
215228
if err != nil {
216229
return err
@@ -405,15 +418,13 @@ func (c *Client) doAdvanceAuthentication(ctx context.Context, username string, p
405418

406419
klog.FromContext(ctx).Info("successfully completed AdvanceAuthentication request to CyberArk Identity; login complete", "username", username)
407420

408-
c.tokenCachedMutex.Lock()
409-
421+
// NB: This assumes we already hold the token cache mutex, which we do in LoginUsernamePassword, so this is safe.
422+
c.tokenCachedTime = time.Now()
410423
c.tokenCached = token{
411424
Username: username,
412425
Token: advanceAuthResponse.Result.Token,
413426
}
414427

415-
c.tokenCachedMutex.Unlock()
416-
417428
return nil
418429
}
419430

internal/cyberark/identity/identity_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestLoginUsernamePassword_RealAPI(t *testing.T) {
5353
arktesting.SkipIfNoEnv(t)
5454
subdomain := os.Getenv("ARK_SUBDOMAIN")
5555
httpClient := http.DefaultClient
56-
services, _, err := servicediscovery.New(httpClient).DiscoverServices(t.Context(), subdomain)
56+
services, _, err := servicediscovery.New(httpClient, subdomain).DiscoverServices(t.Context())
5757
require.NoError(t, err)
5858

5959
loginUsernamePasswordTests(t, func(t testing.TB) inputs {

internal/cyberark/servicediscovery/discovery.go

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"net/url"
1010
"os"
1111
"path"
12+
"sync"
13+
"time"
1214

1315
arkapi "github.com/jetstack/preflight/internal/cyberark/api"
1416
"github.com/jetstack/preflight/pkg/version"
@@ -35,21 +37,34 @@ const (
3537
// users to fetch URLs for various APIs available in CyberArk. This client is specialised to
3638
// fetch only API endpoints, since only API endpoints are required by the Venafi Kubernetes Agent currently.
3739
type Client struct {
38-
client *http.Client
39-
baseURL string
40+
client *http.Client
41+
baseURL string
42+
subdomain string
43+
44+
cachedResponse *Services
45+
cachedTenantID string
46+
cachedResponseTime time.Time
47+
cachedResponseMutex sync.Mutex
4048
}
4149

4250
// New creates a new CyberArk Service Discovery client. If the ARK_DISCOVERY_API
4351
// environment variable is set, it is used as the base URL for the service
4452
// discovery API. Otherwise, the production URL is used.
45-
func New(httpClient *http.Client) *Client {
53+
func New(httpClient *http.Client, subdomain string) *Client {
4654
baseURL := os.Getenv("ARK_DISCOVERY_API")
4755
if baseURL == "" {
4856
baseURL = ProdDiscoveryAPIBaseURL
4957
}
58+
5059
client := &Client{
51-
client: httpClient,
52-
baseURL: baseURL,
60+
client: httpClient,
61+
baseURL: baseURL,
62+
subdomain: subdomain,
63+
64+
cachedResponse: nil,
65+
cachedTenantID: "",
66+
cachedResponseTime: time.Time{},
67+
cachedResponseMutex: sync.Mutex{},
5368
}
5469

5570
return client
@@ -93,17 +108,24 @@ type Services struct {
93108
DiscoveryContext ServiceEndpoint
94109
}
95110

96-
// DiscoverServices fetches from the service discovery service for a given subdomain
111+
// DiscoverServices fetches from the service discovery service for the configured subdomain
97112
// and parses the CyberArk Identity API URL and Inventory API URL.
98113
// It also returns the Tenant ID UUID corresponding to the subdomain.
99-
func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Services, string, error) {
114+
func (c *Client) DiscoverServices(ctx context.Context) (*Services, string, error) {
115+
c.cachedResponseMutex.Lock()
116+
defer c.cachedResponseMutex.Unlock()
117+
118+
if c.cachedResponse != nil && time.Since(c.cachedResponseTime) < 1*time.Hour {
119+
return c.cachedResponse, c.cachedTenantID, nil
120+
}
121+
100122
u, err := url.Parse(c.baseURL)
101123
if err != nil {
102124
return nil, "", fmt.Errorf("invalid base URL for service discovery: %w", err)
103125
}
104126

105127
u.Path = path.Join(u.Path, "api/public/tenant-discovery")
106-
u.RawQuery = url.Values{"bySubdomain": []string{subdomain}}.Encode()
128+
u.RawQuery = url.Values{"bySubdomain": []string{c.subdomain}}.Encode()
107129

108130
endpoint := u.String()
109131

@@ -127,7 +149,7 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi
127149
// a 404 error is returned with an empty JSON body "{}" if the subdomain is unknown; at the time of writing, we haven't observed
128150
// any other errors and so we can't special case them
129151
if resp.StatusCode == http.StatusNotFound {
130-
return nil, "", fmt.Errorf("got an HTTP 404 response from service discovery; maybe the subdomain %q is incorrect or does not exist?", subdomain)
152+
return nil, "", fmt.Errorf("got an HTTP 404 response from service discovery; maybe the subdomain %q is incorrect or does not exist?", c.subdomain)
131153
}
132154

133155
return nil, "", fmt.Errorf("got unexpected status code %s from request to service discovery API", resp.Status)
@@ -167,8 +189,14 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi
167189
}
168190
//TODO: Should add a check for discoveryContextAPI too?
169191

170-
return &Services{
192+
services := &Services{
171193
Identity: ServiceEndpoint{API: identityAPI},
172194
DiscoveryContext: ServiceEndpoint{API: discoveryContextAPI},
173-
}, discoveryResp.TenantID, nil
195+
}
196+
197+
c.cachedResponse = services
198+
c.cachedTenantID = discoveryResp.TenantID
199+
c.cachedResponseTime = time.Now()
200+
201+
return services, discoveryResp.TenantID, nil
174202
}

internal/cyberark/servicediscovery/discovery_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ func Test_DiscoverIdentityAPIURL(t *testing.T) {
6464
},
6565
})
6666

67-
client := New(httpClient)
67+
client := New(httpClient, testSpec.subdomain)
6868

69-
services, _, err := client.DiscoverServices(ctx, testSpec.subdomain)
69+
services, _, err := client.DiscoverServices(ctx)
7070
if testSpec.expectedError != nil {
7171
assert.EqualError(t, err, testSpec.expectedError.Error())
7272
assert.Nil(t, services)

0 commit comments

Comments
 (0)