Skip to content

Commit 7acd5c9

Browse files
committed
add cyberark client and mock server
Signed-off-by: Tim Ramlot <[email protected]>
1 parent c080864 commit 7acd5c9

File tree

2 files changed

+323
-0
lines changed

2 files changed

+323
-0
lines changed

pkg/client/client_cyberark.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package client
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/x509"
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"time"
12+
13+
"k8s.io/client-go/transport"
14+
15+
"github.com/jetstack/preflight/api"
16+
"github.com/jetstack/preflight/pkg/version"
17+
)
18+
19+
type CyberArkClient struct {
20+
baseURL string
21+
22+
// Used to make HTTP requests to CyberArk.
23+
Client *http.Client
24+
}
25+
26+
func NewCyberArkClient(trustedCAs *x509.CertPool, baseURL string) (*CyberArkClient, error) {
27+
cyberClient := &http.Client{}
28+
tr := http.DefaultTransport.(*http.Transport).Clone()
29+
if trustedCAs != nil {
30+
tr.TLSClientConfig.RootCAs = trustedCAs
31+
}
32+
cyberClient.Transport = transport.DebugWrappers(tr)
33+
34+
return &CyberArkClient{
35+
baseURL: baseURL,
36+
Client: cyberClient,
37+
}, nil
38+
}
39+
40+
// `opts.ClusterName` and `opts.ClusterDescription` are the only values used
41+
// from the Options struct. OrgID and ClusterID are not used by CyberArk.
42+
func (c *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, opts Options) error {
43+
if opts.ClusterName == "" {
44+
return fmt.Errorf("programmer mistake: the cluster name (aka `cluster_id` in the config file) cannot be left empty")
45+
}
46+
47+
presignedUploadURL, err := c.retrievePresignedUploadURL(ctx, "TODO-token", "TODO-checksum", opts)
48+
if err != nil {
49+
return err
50+
}
51+
52+
// POST to presigned URL
53+
payload := api.DataReadingsPost{
54+
AgentMetadata: nil,
55+
DataGatherTime: time.Now().UTC(),
56+
DataReadings: readings,
57+
}
58+
59+
encodedBody := &bytes.Buffer{}
60+
if err := json.NewEncoder(encodedBody).Encode(payload); err != nil {
61+
return err
62+
}
63+
64+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, presignedUploadURL, encodedBody)
65+
if err != nil {
66+
return err
67+
}
68+
69+
req.Header.Set("Content-Type", "application/json")
70+
version.SetUserAgent(req)
71+
72+
res, err := c.Client.Do(req)
73+
if err != nil {
74+
return err
75+
}
76+
defer res.Body.Close()
77+
78+
if code := res.StatusCode; code < 200 || code >= 300 {
79+
errorContent := ""
80+
body, _ := io.ReadAll(res.Body)
81+
if body != nil {
82+
errorContent = string(body)
83+
}
84+
85+
return fmt.Errorf("received response with status code %d. Body: [%s]", code, errorContent)
86+
}
87+
88+
return nil
89+
}
90+
91+
// POST to api/data/kubernetes/upload and retrieve presigned URL
92+
func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, token string, checksum string, opts Options) (string, error) {
93+
request := struct {
94+
ClusterID string `json:"cluster_id"`
95+
ClusterDescription string `json:"Cluster_description"`
96+
Checksum string `json:"checksum_sha256"`
97+
}{
98+
ClusterID: opts.ClusterName,
99+
ClusterDescription: opts.ClusterDescription,
100+
Checksum: checksum,
101+
}
102+
103+
encodedBody := &bytes.Buffer{}
104+
if err := json.NewEncoder(encodedBody).Encode(request); err != nil {
105+
return "", err
106+
}
107+
108+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL(c.baseURL, "/api/data/kubernetes/upload"), encodedBody)
109+
if err != nil {
110+
return "", err
111+
}
112+
113+
req.Header.Set("Content-Type", "application/json")
114+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
115+
version.SetUserAgent(req)
116+
117+
res, err := c.Client.Do(req)
118+
if err != nil {
119+
return "", err
120+
}
121+
defer res.Body.Close()
122+
123+
if code := res.StatusCode; code < 200 || code >= 300 {
124+
errorContent := ""
125+
body, _ := io.ReadAll(res.Body)
126+
if body != nil {
127+
errorContent = string(body)
128+
}
129+
130+
return "", fmt.Errorf("received response with status code %d. Body: [%s]", code, errorContent)
131+
}
132+
133+
response := struct {
134+
URL string `json:"url"`
135+
}{}
136+
137+
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
138+
return "", err
139+
}
140+
141+
return response.URL, nil
142+
}

pkg/client/client_cyberark_test.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package client_test
2+
3+
import (
4+
"context"
5+
"crypto/x509"
6+
"encoding/json"
7+
"encoding/pem"
8+
"io"
9+
"net/http"
10+
"net/http/httptest"
11+
"testing"
12+
"time"
13+
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
17+
"github.com/jetstack/preflight/api"
18+
"github.com/jetstack/preflight/pkg/client"
19+
)
20+
21+
func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
22+
type testCase struct {
23+
name string
24+
handler http.Handler
25+
readings []*api.DataReading
26+
opts client.Options
27+
assertion func(t *testing.T, err error)
28+
}
29+
30+
defaultReadings := []*api.DataReading{
31+
{
32+
ClusterID: "test-cluster",
33+
DataGatherer: "test-gatherer",
34+
Timestamp: api.Time{Time: time.Now()},
35+
Data: map[string]interface{}{"test": "data"},
36+
SchemaVersion: "v1",
37+
},
38+
}
39+
defaultOpts := client.Options{
40+
ClusterName: "test-cluster",
41+
ClusterDescription: "Test cluster description",
42+
}
43+
44+
tests := []testCase{
45+
{
46+
name: "successful upload",
47+
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
48+
switch r.URL.Path {
49+
case "/api/data/kubernetes/upload":
50+
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
51+
assert.Equal(t, "Bearer TODO-token", r.Header.Get("Authorization"))
52+
body, err := io.ReadAll(r.Body)
53+
require.NoError(t, err)
54+
var req struct {
55+
ClusterID string `json:"cluster_id"`
56+
ClusterDescription string `json:"Cluster_description"`
57+
Checksum string `json:"checksum_sha256"`
58+
}
59+
err = json.Unmarshal(body, &req)
60+
require.NoError(t, err)
61+
assert.Equal(t, "test-cluster", req.ClusterID)
62+
assert.Equal(t, "Test cluster description", req.ClusterDescription)
63+
assert.Equal(t, "TODO-checksum", req.Checksum)
64+
presignedURL := "https://" + r.Host + "/presigned-upload"
65+
w.Header().Set("Content-Type", "application/json")
66+
require.NoError(t, json.NewEncoder(w).Encode(struct {
67+
URL string `json:"url"`
68+
}{presignedURL}))
69+
case "/presigned-upload":
70+
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
71+
body, err := io.ReadAll(r.Body)
72+
require.NoError(t, err)
73+
var payload api.DataReadingsPost
74+
err = json.Unmarshal(body, &payload)
75+
require.NoError(t, err)
76+
assert.Nil(t, payload.AgentMetadata)
77+
assert.NotZero(t, payload.DataGatherTime)
78+
assert.Len(t, payload.DataReadings, 1)
79+
assert.Equal(t, "test-cluster", payload.DataReadings[0].ClusterID)
80+
assert.Equal(t, "test-gatherer", payload.DataReadings[0].DataGatherer)
81+
w.WriteHeader(http.StatusOK)
82+
default:
83+
t.Errorf("Unexpected request path: %s", r.URL.Path)
84+
w.WriteHeader(http.StatusNotFound)
85+
}
86+
}),
87+
readings: defaultReadings,
88+
opts: defaultOpts,
89+
assertion: func(t *testing.T, err error) {
90+
assert.NoError(t, err)
91+
},
92+
},
93+
{
94+
name: "error when cluster name is empty",
95+
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
96+
readings: defaultReadings,
97+
opts: client.Options{ClusterName: ""},
98+
assertion: func(t *testing.T, err error) {
99+
assert.Error(t, err)
100+
assert.Contains(t, err.Error(), "programmer mistake: the cluster name (aka `cluster_id` in the config file) cannot be left empty")
101+
},
102+
},
103+
{
104+
name: "error when presigned URL request fails",
105+
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
106+
if r.URL.Path == "/api/data/kubernetes/upload" {
107+
w.WriteHeader(http.StatusInternalServerError)
108+
_, _ = w.Write([]byte("Internal server error"))
109+
return
110+
}
111+
w.WriteHeader(http.StatusNotFound)
112+
}),
113+
readings: defaultReadings,
114+
opts: defaultOpts,
115+
assertion: func(t *testing.T, err error) {
116+
assert.Error(t, err)
117+
assert.Contains(t, err.Error(), "received response with status code 500. Body: [Internal server error]")
118+
},
119+
},
120+
{
121+
name: "error when upload to presigned URL fails",
122+
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
123+
if r.URL.Path == "/api/data/kubernetes/upload" {
124+
presignedURL := "https://" + r.Host + "/presigned-upload"
125+
require.NoError(t, json.NewEncoder(w).Encode(struct {
126+
URL string `json:"url"`
127+
}{presignedURL}))
128+
return
129+
}
130+
if r.URL.Path == "/presigned-upload" {
131+
w.WriteHeader(http.StatusBadRequest)
132+
_, _ = w.Write([]byte("Bad request"))
133+
return
134+
}
135+
w.WriteHeader(http.StatusNotFound)
136+
}),
137+
readings: defaultReadings,
138+
opts: defaultOpts,
139+
assertion: func(t *testing.T, err error) {
140+
assert.Error(t, err)
141+
assert.Contains(t, err.Error(), "received response with status code 400. Body: [Bad request]")
142+
},
143+
},
144+
{
145+
name: "error when presigned URL response is invalid JSON",
146+
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
147+
if r.URL.Path == "/api/data/kubernetes/upload" {
148+
w.Header().Set("Content-Type", "application/json")
149+
_, _ = w.Write([]byte("invalid json"))
150+
return
151+
}
152+
w.WriteHeader(http.StatusNotFound)
153+
}),
154+
readings: defaultReadings,
155+
opts: defaultOpts,
156+
assertion: func(t *testing.T, err error) {
157+
assert.Error(t, err)
158+
assert.Contains(t, err.Error(), "invalid character")
159+
},
160+
},
161+
}
162+
163+
for _, tc := range tests {
164+
t.Run(tc.name, func(t *testing.T) {
165+
server := httptest.NewTLSServer(tc.handler)
166+
defer server.Close()
167+
168+
certPool := x509.NewCertPool()
169+
require.True(t, certPool.AppendCertsFromPEM(pem.EncodeToMemory(&pem.Block{
170+
Type: "CERTIFICATE",
171+
Bytes: server.TLS.Certificates[0].Certificate[0],
172+
})))
173+
174+
cyberArkClient, err := client.NewCyberArkClient(certPool, server.URL)
175+
require.NoError(t, err)
176+
177+
postErr := cyberArkClient.PostDataReadingsWithOptions(context.TODO(), tc.readings, tc.opts)
178+
tc.assertion(t, postErr)
179+
})
180+
}
181+
}

0 commit comments

Comments
 (0)