Skip to content

Commit fa0a0db

Browse files
API key based authentication support (#232)
* added support for API auth for V3 * fix: update module path to match repository owner * Revert "fix: update module path to match repository owner" This reverts commit cc06347. * fix: update authentication handling to use API key directly * refactor: remove unused AuthType and constants for cleaner code * fix: add missing newline * plumb API key into each of v4 client instances * refactor: replace constant usage for API key header with local definition * chore: update changelog to include API key authentication support * fix tests * added tests for client creation for API key case * refactor: extract authentication header logic into SetAuthHeader function * make functions not exported * fix: reorder import statements for clarity * refactor: consolidate authentication header logic into a single function
1 parent 6623339 commit fa0a0db

File tree

7 files changed

+141
-30
lines changed

7 files changed

+141
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Added
99
- Added witness configuration to recovery plan spec.
1010
- Added support for creating, deleting, and listing idempotence identifiers.
11+
- Added support for authenticating using API key based authentication.
1112

1213
### Changed
1314
- Updated the v3 Cluster spec and status structs to match latest swagger spec

internal/client.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,17 @@ import (
2121
"github.com/hashicorp/go-cleanhttp"
2222
"go.uber.org/zap"
2323

24-
"github.com/nutanix-cloud-native/prism-go-client"
24+
prismgoclient "github.com/nutanix-cloud-native/prism-go-client"
2525
)
2626

2727
type Scheme string
2828

2929
const (
30-
defaultBaseURL = "%s://%s/"
31-
mediaType = "application/json"
32-
formEncodedType = "application/x-www-form-urlencoded"
33-
octetStreamType = "application/octet-stream"
30+
defaultBaseURL = "%s://%s/"
31+
mediaType = "application/json"
32+
formEncodedType = "application/x-www-form-urlencoded"
33+
octetStreamType = "application/octet-stream"
34+
ntnxAPIKeyHeaderKey = "X-ntnx-api-key"
3435

3536
SchemeHTTP Scheme = "http"
3637
SchemeHTTPS Scheme = "https"
@@ -229,6 +230,14 @@ func NewClient(opts ...ClientOption) (*Client, error) {
229230
return c, nil
230231
}
231232

233+
func decorateRequestWithAuthHeaders(req *http.Request, c *prismgoclient.Credentials) {
234+
if c.APIKey != "" {
235+
decorateRequestWithAPIKeyHeaders(req, c.APIKey)
236+
} else {
237+
decorateRequestWithBasicAuthHeaders(req, c.Username, c.Password)
238+
}
239+
}
240+
232241
// NewRequest creates a request
233242
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
234243
req, err := c.NewUnAuthRequest(method, urlStr, body)
@@ -239,20 +248,25 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
239248
if c.cookies != nil {
240249
decorateRequestWithCookies(req, c.cookies)
241250
} else {
242-
decorateRequestWithBasicAuthHeaders(req, c.credentials.Username, c.credentials.Password)
251+
decorateRequestWithAuthHeaders(req, c.credentials)
243252
}
244253

245254
return req, nil
246255
}
247256

257+
// decorateRequestWithAPIKeyHeaders adds the API key to the request header
258+
func decorateRequestWithAPIKeyHeaders(req *http.Request, apiKey string) {
259+
req.Header.Add(ntnxAPIKeyHeaderKey, apiKey)
260+
}
261+
248262
func (c *Client) refreshCookies(ctx context.Context) error {
249263
req, err := c.NewUnAuthRequest(http.MethodGet, "/users/me", nil)
250264
if err != nil {
251265
return err
252266
}
253267

254268
req = req.WithContext(ctx)
255-
decorateRequestWithBasicAuthHeaders(req, c.credentials.Username, c.credentials.Password)
269+
decorateRequestWithAuthHeaders(req, c.credentials)
256270
resp, err := c.httpClient.Do(req)
257271
if err != nil {
258272
return err

structs.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package prismgoclient
22

3-
// Credentials needed username and password
3+
// Credentials can include either username and password for basic authentication
4+
// or an API key for API key-based authentication
45
type Credentials struct {
56
URL string
7+
APIKey string
68
Username string
79
Password string
810
Endpoint string

v3/v3.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"net/http"
88

99
"github.com/go-logr/logr"
10-
"github.com/nutanix-cloud-native/prism-go-client"
10+
prismgoclient "github.com/nutanix-cloud-native/prism-go-client"
1111
"github.com/nutanix-cloud-native/prism-go-client/internal"
1212
)
1313

@@ -84,8 +84,14 @@ func WithUserAgent(userAgent string) ClientOption {
8484

8585
// NewV3Client return a internal to operate V3 resources
8686
func NewV3Client(credentials prismgoclient.Credentials, opts ...ClientOption) (*Client, error) {
87-
if credentials.Username == "" || credentials.Password == "" || credentials.Endpoint == "" {
88-
return nil, fmt.Errorf("username, password and endpoint are required")
87+
if credentials.APIKey != "" {
88+
if credentials.Endpoint == "" {
89+
return nil, fmt.Errorf("endpoint is required for api key auth")
90+
}
91+
} else {
92+
if credentials.Username == "" || credentials.Password == "" || credentials.Endpoint == "" {
93+
return nil, fmt.Errorf("username, password and endpoint are required for basic auth")
94+
}
8995
}
9096

9197
v3Client := &Client{

v3/v3_test.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import (
88
"github.com/stretchr/testify/assert"
99
"github.com/stretchr/testify/require"
1010

11-
"github.com/nutanix-cloud-native/prism-go-client"
11+
prismgoclient "github.com/nutanix-cloud-native/prism-go-client"
1212
"github.com/nutanix-cloud-native/prism-go-client/internal/testhelpers"
1313
)
1414

15-
func TestNewV3Client(t *testing.T) {
15+
func TestNewV3ClientBasicAuth(t *testing.T) {
1616
// verifies positive client creation
1717
cred := prismgoclient.Credentials{
1818
URL: "foo.com",
@@ -40,7 +40,38 @@ func TestNewV3Client(t *testing.T) {
4040

4141
v3Client, err = NewV3Client(cred)
4242
assert.Nil(t, v3Client)
43-
assert.EqualError(t, err, "username, password and endpoint are required")
43+
assert.EqualError(t, err, "username, password and endpoint are required for basic auth")
44+
}
45+
46+
func TestNewV3ClientAPIKey(t *testing.T) {
47+
// verifies positive client creation
48+
cred := prismgoclient.Credentials{
49+
URL: "foo.com",
50+
Port: "",
51+
Endpoint: "0.0.0.0",
52+
Insecure: true,
53+
FoundationEndpoint: "10.0.0.0",
54+
FoundationPort: "8000",
55+
RequiredFields: nil,
56+
APIKey: "my-api-key",
57+
}
58+
v3Client, err := NewV3Client(cred)
59+
assert.NoError(t, err)
60+
assert.NotNil(t, v3Client)
61+
62+
// verify missing client scenario
63+
cred = prismgoclient.Credentials{
64+
URL: "foo.com",
65+
Insecure: true,
66+
RequiredFields: map[string][]string{
67+
"prism_central": {"username", "password", "endpoint"},
68+
},
69+
APIKey: "my-api-key",
70+
}
71+
72+
v3Client, err = NewV3Client(cred)
73+
assert.Nil(t, v3Client)
74+
assert.EqualError(t, err, "endpoint is required for api key auth")
4475
}
4576

4677
func TestNewV3ClientWithPEMEncodedCertBundle(t *testing.T) {

v4/v4.go

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,25 @@ import (
2323
const (
2424
defaultEndpointPort = "9440"
2525
authorizationHeader = "Authorization"
26+
ntnxAPIKeyHeaderKey = "X-ntnx-api-key"
2627
)
2728

29+
// apiClient is an interface that defines methods for adding default headers to the API client.
30+
type apiClient interface {
31+
AddDefaultHeader(key, value string)
32+
}
33+
34+
// setAuthHeader sets the authentication header for the API client based on the provided credentials.
35+
func setAuthHeader(apiClient apiClient, credentials prismgoclient.Credentials) {
36+
if credentials.APIKey != "" {
37+
apiClient.AddDefaultHeader(ntnxAPIKeyHeaderKey, credentials.APIKey)
38+
} else {
39+
apiClient.AddDefaultHeader(
40+
authorizationHeader,
41+
fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", credentials.Username, credentials.Password)))))
42+
}
43+
}
44+
2845
// Client manages the V4 API
2946
type Client struct {
3047
CategoriesApiInstance *prismApi.CategoriesApi
@@ -48,8 +65,14 @@ type ClientOption func(*Client) error
4865

4966
// NewV4Client return an internal to operate V4 resources
5067
func NewV4Client(credentials prismgoclient.Credentials, opts ...ClientOption) (*Client, error) {
51-
if credentials.Username == "" || credentials.Password == "" || credentials.Endpoint == "" {
52-
return nil, fmt.Errorf("username, password and endpoint are required")
68+
if credentials.APIKey != "" {
69+
if credentials.Endpoint == "" {
70+
return nil, fmt.Errorf("endpoint is required for api key auth")
71+
}
72+
} else {
73+
if credentials.Username == "" || credentials.Password == "" || credentials.Endpoint == "" {
74+
return nil, fmt.Errorf("username, password and endpoint are required for basic auth")
75+
}
5376
}
5477

5578
v4Client := &Client{}
@@ -90,8 +113,7 @@ func initVmApiInstance(v4Client *Client, credentials prismgoclient.Credentials)
90113
apiClientInstance.VerifySSL = !credentials.Insecure
91114
apiClientInstance.Host = ep.host
92115
apiClientInstance.Port = ep.port
93-
apiClientInstance.AddDefaultHeader(
94-
authorizationHeader, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", credentials.Username, credentials.Password)))))
116+
setAuthHeader(apiClientInstance, credentials)
95117
v4Client.VmApiInstance = vmApi.NewVmApi(apiClientInstance)
96118
v4Client.ImagesApiInstance = vmApi.NewImagesApi(apiClientInstance)
97119
return nil
@@ -106,8 +128,7 @@ func initClusterApiInstance(v4Client *Client, credentials prismgoclient.Credenti
106128
apiClientInstance.VerifySSL = !credentials.Insecure
107129
apiClientInstance.Host = ep.host
108130
apiClientInstance.Port = ep.port
109-
apiClientInstance.AddDefaultHeader(
110-
authorizationHeader, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", credentials.Username, credentials.Password)))))
131+
setAuthHeader(apiClientInstance, credentials)
111132
v4Client.ClustersApiInstance = clusterApi.NewClustersApi(apiClientInstance)
112133
return nil
113134
}
@@ -121,8 +142,7 @@ func initPrismApiInstance(v4Client *Client, credentials prismgoclient.Credential
121142
apiClientInstance.VerifySSL = !credentials.Insecure
122143
apiClientInstance.Host = ep.host
123144
apiClientInstance.Port = ep.port
124-
apiClientInstance.AddDefaultHeader(
125-
authorizationHeader, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", credentials.Username, credentials.Password)))))
145+
setAuthHeader(apiClientInstance, credentials)
126146
v4Client.TasksApiInstance = prismApi.NewTasksApi(apiClientInstance)
127147
v4Client.CategoriesApiInstance = prismApi.NewCategoriesApi(apiClientInstance)
128148
return nil
@@ -137,8 +157,7 @@ func initSubnetApiInstance(v4Client *Client, credentials prismgoclient.Credentia
137157
apiClientInstance.SetVerifySSL(!credentials.Insecure)
138158
apiClientInstance.Host = ep.host
139159
apiClientInstance.Port = ep.port
140-
apiClientInstance.AddDefaultHeader(
141-
authorizationHeader, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", credentials.Username, credentials.Password)))))
160+
setAuthHeader(apiClientInstance, credentials)
142161
v4Client.SubnetsApiInstance = networkingApi.NewSubnetsApi(apiClientInstance)
143162
v4Client.SubnetIPReservationApi = networkingApi.NewSubnetIPReservationApi(apiClientInstance)
144163
return nil
@@ -153,8 +172,7 @@ func initStorageApiInstance(v4Client *Client, credentials prismgoclient.Credenti
153172
apiClientInstance.SetVerifySSL(!credentials.Insecure)
154173
apiClientInstance.Host = ep.host
155174
apiClientInstance.Port = ep.port
156-
apiClientInstance.AddDefaultHeader(
157-
authorizationHeader, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", credentials.Username, credentials.Password)))))
175+
setAuthHeader(apiClientInstance, credentials)
158176
v4Client.StorageContainerAPI = clusterApi.NewStorageContainersApi(apiClientInstance)
159177
return nil
160178
}
@@ -168,8 +186,7 @@ func initVolumesApiInstance(v4Client *Client, credentials prismgoclient.Credenti
168186
apiClientInstance.SetVerifySSL(!credentials.Insecure)
169187
apiClientInstance.Host = ep.host
170188
apiClientInstance.Port = ep.port
171-
apiClientInstance.AddDefaultHeader(
172-
authorizationHeader, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", credentials.Username, credentials.Password)))))
189+
setAuthHeader(apiClientInstance, credentials)
173190
v4Client.VolumeGroupsApiInstance = volumesApi.NewVolumeGroupsApi(apiClientInstance)
174191
return nil
175192
}

v4/v4_test.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"github.com/stretchr/testify/assert"
88
)
99

10-
func TestNewV4Client(t *testing.T) {
10+
func TestNewV4ClientBasicAuth(t *testing.T) {
1111
// verifies positive client creation
1212
cred := prismgoclient.Credentials{
1313
URL: "foo.com",
@@ -44,5 +44,45 @@ func TestNewV4Client(t *testing.T) {
4444

4545
v4Client, err = NewV4Client(cred)
4646
assert.Nil(t, v4Client)
47-
assert.EqualError(t, err, "username, password and endpoint are required")
47+
assert.EqualError(t, err, "username, password and endpoint are required for basic auth")
48+
}
49+
50+
func TestNewV4Client(t *testing.T) {
51+
// verifies positive client creation
52+
cred := prismgoclient.Credentials{
53+
URL: "foo.com",
54+
APIKey: "my-api-key",
55+
Port: "",
56+
Endpoint: "0.0.0.0",
57+
Insecure: true,
58+
FoundationEndpoint: "10.0.0.0",
59+
FoundationPort: "8000",
60+
RequiredFields: nil,
61+
}
62+
v4Client, err := NewV4Client(cred)
63+
assert.NoError(t, err)
64+
assert.NotNil(t, v4Client)
65+
assert.NotNil(t, v4Client.VmApiInstance)
66+
assert.NotNil(t, v4Client.ImagesApiInstance)
67+
assert.NotNil(t, v4Client.SubnetsApiInstance)
68+
assert.NotNil(t, v4Client.SubnetIPReservationApi)
69+
assert.NotNil(t, v4Client.ClustersApiInstance)
70+
assert.NotNil(t, v4Client.TasksApiInstance)
71+
assert.NotNil(t, v4Client.StorageContainerAPI)
72+
assert.NotNil(t, v4Client.CategoriesApiInstance)
73+
assert.NotNil(t, v4Client.VolumeGroupsApiInstance)
74+
75+
// verify missing client scenario
76+
cred = prismgoclient.Credentials{
77+
URL: "foo.com",
78+
Insecure: true,
79+
RequiredFields: map[string][]string{
80+
"prism_central": {"username", "password", "endpoint"},
81+
},
82+
APIKey: "my-api-key",
83+
}
84+
85+
v4Client, err = NewV4Client(cred)
86+
assert.Nil(t, v4Client)
87+
assert.EqualError(t, err, "endpoint is required for api key auth")
4888
}

0 commit comments

Comments
 (0)