Skip to content

Commit c9db5ce

Browse files
andrew-sumnerdt-andrewsGullapalliAkhil
authored
[Feat] API Key and custom headers as an alternative method of authorisation (#1062)
Co-authored-by: Andrew Sumner <andrew.sumner@datatorque.com> Co-authored-by: Gullapalli Akhil Sai <akhilsai.gullapalli@nutanix.com>
1 parent d812199 commit c9db5ce

24 files changed

Lines changed: 904 additions & 336 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,10 @@ NDB_ENDPOINT="x.x.x.x"
1818
NDB_USERNAME="x.x.x.x"
1919
NDB_PASSWORD="x.x.x.x"
2020

21+
# API Key (the `X-Ntnx-Api-Key` header will be used instead of Basic Authentication) and Custom Headers (eg for Cloudflare Access)
22+
# NUTANIX_API_KEY="xxx"
23+
# NUTANIX_HEADER_CF_ACCESS_CLIENT_ID="xxx.access"
24+
# NUTANIX_HEADER_CF_ACCESS_CLIENT_SECRET="xxx"
25+
2126
# Terraform / test log level
2227
TF_LOG=ERROR

CONTRIBUTING.md

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -81,50 +81,67 @@ under:
8181
Local Nutanix provider plugin will be used after `terraform init`
8282
command execution in Terraform template directory
8383

84-
8584
### Running tests of provider
8685

8786
For running unit tests:
87+
8888
```sh
8989
make test
9090
```
9191

9292
For running integration tests:
9393

9494
1. Add environment variables for setup related details:
95-
```ssh
96-
export NUTANIX_USERNAME="<username>"
97-
export NUTANIX_PASSWORD="<password>"
98-
export NUTANIX_INSECURE=true
99-
export NUTANIX_PORT=9440
100-
export NUTANIX_ENDPOINT="<pc-ip>"
101-
export NUTANIX_STORAGE_CONTAINER="<storage-container-uuid-for-vm-tests>"
102-
export FOUNDATION_ENDPOINT="<foundation-vm-ip-for-foundation-related-tests>"
103-
export FOUNDATION_PORT=8000
104-
export NOS_IMAGE_TEST_URL="<test-image-url>"
105-
export NDB_ENDPOINT="<ndb-ip>"
106-
export NDB_USERNAME="<username>"
107-
export NDB_PASSWORD="<password>"
108-
```
95+
96+
```sh
97+
export NUTANIX_USERNAME="<username>"
98+
export NUTANIX_PASSWORD="<password>"
99+
export NUTANIX_INSECURE=true
100+
export NUTANIX_PORT=9440
101+
export NUTANIX_ENDPOINT="<pc-ip>"
102+
export NUTANIX_STORAGE_CONTAINER="<storage-container-uuid-for-vm-tests>"
103+
export FOUNDATION_ENDPOINT="<foundation-vm-ip-for-foundation-related-tests>"
104+
export FOUNDATION_PORT=8000
105+
export NOS_IMAGE_TEST_URL="<test-image-url>"
106+
export NDB_ENDPOINT="<ndb-ip>"
107+
export NDB_USERNAME="<username>"
108+
export NDB_PASSWORD="<password>"
109+
```
110+
111+
Alternatively, you can authenticate using an API key instead of username/password:
112+
113+
```sh
114+
export NUTANIX_API_KEY="<api-key>"
115+
```
116+
117+
Custom HTTP headers (e.g. for Cloudflare Access) can be injected via environment variables with the `NUTANIX_HEADER_` prefix:
118+
119+
```sh
120+
export NUTANIX_HEADER_CF_ACCESS_CLIENT_ID="<client-id>"
121+
export NUTANIX_HEADER_CF_ACCESS_CLIENT_SECRET="<client-secret>"
122+
```
109123

110124
2. Some tests need setup related constants for resource creation. So add/replace details in test_config.json (for pc tests) and test_foundation_config.json (for foundation and foundation central tests and NDB)
111125

112126
3. To run all tests:
113-
```ssh
114-
make testacc
115-
```
127+
128+
```sh
129+
make testacc
130+
```
116131

117132
4. To run specific tests:
118-
```ssh
119-
export TESTARGS='-run=TestAccNutanixPbr_WithSourceExternalDestinationNetwork'
120-
make testacc
121-
```
133+
134+
```sh
135+
export TESTARGS='-run=TestAccNutanixPbr_WithSourceExternalDestinationNetwork'
136+
make testacc
137+
```
122138

123139
5. To run collection of tests:
124-
``` ssh
125-
export TESTARGS='-run=TestAccNutanixPbr*'
126-
make testacc
127-
```
140+
141+
```sh
142+
export TESTARGS='-run=TestAccNutanixPbr*'
143+
make testacc
144+
```
128145

129146
### Common Issues using the development binary.
130147

README.md

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,16 @@ Long term, once this is upstream, no pre-compiled binaries will be needed, as te
108108
The following keys can be used to configure the provider.
109109

110110
* **endpoint** - (Required) IP address for the Nutanix Prism Central.
111-
* **username** - (Required) Username for Nutanix Prism Central. Could be local cluster auth (e.g. `auth`) or directory auth.
112-
* **password** - (Required) Password for the provided username.
113-
* **port** - (Optional) Port for the Nutanix Prism Central. Default port is 9440.
111+
* **username** - (Optional) Username for Nutanix Prism Central. Could be local cluster auth (e.g. `auth`) or directory auth. Required if `api_key` is not set.
112+
* **password** - (Optional) Password for the provided username. Required if `api_key` is not set.
113+
* **api_key** - (Optional) API key for Prism Central authentication. Can be used as an alternative to `username`/`password` when connecting to a Prism Central instance. **Not supported by Prism Elements**, which requires `username` and `password`. When set, the `X-Ntnx-Api-Key` header is used instead of Basic Authentication.
114+
* **port** - (Optional) Port for the Nutanix Prism Central. Default port is 9440. Can also be set via the `NUTANIX_PORT` environment variable.
114115
* **insecure** - (Optional) Explicitly allow the provider to perform insecure SSL requests. If omitted, default value is false.
115-
* **wait_timeout** - (optional) Set if you know that the creation o update of a resource may take long time (minutes).
116+
* **wait_timeout** - (Optional) Set if you know that the creation or update of a resource may take long time (minutes).
117+
* **custom_headers** - (Optional) Map of custom HTTP headers to add to all API requests. Useful for environments that require additional headers such as Cloudflare Access service tokens. Headers can also be set via environment variables with the `NUTANIX_HEADER_` prefix (e.g. `NUTANIX_HEADER_CF_ACCESS_CLIENT_ID` becomes `Cf-Access-Client-Id`). Values defined in config take precedence over environment variables.
116118

117119
```hcl
120+
# Basic authentication
118121
provider "nutanix" {
119122
username = "admin"
120123
password = "myPassword"
@@ -123,9 +126,22 @@ provider "nutanix" {
123126
insecure = true
124127
wait_timeout = 10
125128
}
129+
130+
# API key authentication with custom headers (e.g. Cloudflare Access)
131+
provider "nutanix" {
132+
api_key = "my-api-key"
133+
port = 443
134+
endpoint = "10.36.7.201"
135+
insecure = true
136+
wait_timeout = 10
137+
custom_headers = {
138+
"Cf-Access-Client-Id" = "my-client-id"
139+
"Cf-Access-Client-Secret" = "my-client-secret"
140+
}
141+
}
126142
```
127143

128-
## From terraform-provider-nutanix v1.5.0-beta :
144+
## From terraform-provider-nutanix v1.5.0-beta
129145

130146
The following keys can be used to configure the provider.
131147

@@ -151,7 +167,7 @@ provider "nutanix" {
151167
}
152168
```
153169

154-
## Additional fields for using Nutanix Database Service:
170+
## Additional fields for using Nutanix Database Service
155171

156172
* **ndb_username** - (Optional) Username of Nutanix Database Service server
157173
* **ndb_password** - (Optional) Password of Nutanix Database Service server
@@ -166,10 +182,10 @@ provider "nutanix" {
166182
```
167183

168184
### Provider Configuration Requirements & Warnings
169-
From foundation getting released in 1.5.0-beta, provider configuration will accomodate prism central and foundation apis connection details. **It will show warnings for disabled api connections as per the attributes given in provider configuration in above mentioned format**. The below are the required attributes for corresponding provider componenets :
170-
* endpoint, username and password are required fields for using Prism Central & Karbon based resources and data sources
171-
* foundation_endpoint is required field for using Foundation based resources and data sources
172-
* ndb_username, ndb_password and ndb_endpoint are required fields for using NDB based resources and data sources
185+
From foundation getting released in 1.5.0-beta, provider configuration will accommodate Prism Central and foundation API connection details. **It will show warnings for disabled API connections as per the attributes given in provider configuration in above mentioned format**. The below are the required attributes for corresponding provider components:
186+
* `endpoint` and either (`username` + `password`) or `api_key` are required for using Prism Central & Karbon based resources and data sources.
187+
* `foundation_endpoint` is required field for using Foundation based resources and data sources
188+
* `ndb_username`, `ndb_password` and `ndb_endpoint` are required fields for using NDB based resources and data sources
173189

174190

175191
## Resources

nutanix/acctest/acctest.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,29 @@ func TestProviderImpl(t *testing.T) {
3636
}
3737

3838
func TestAccPreCheck(t *testing.T) {
39-
if os.Getenv("NUTANIX_USERNAME") == "" ||
40-
os.Getenv("NUTANIX_PASSWORD") == "" ||
41-
os.Getenv("NUTANIX_INSECURE") == "" ||
39+
// Check common required variables
40+
if os.Getenv("NUTANIX_INSECURE") == "" ||
4241
os.Getenv("NUTANIX_PORT") == "" ||
4342
os.Getenv("NUTANIX_ENDPOINT") == "" ||
4443
os.Getenv("NUTANIX_STORAGE_CONTAINER") == "" {
45-
t.Fatal("`NUTANIX_USERNAME`,`NUTANIX_PASSWORD`,`NUTANIX_INSECURE`,`NUTANIX_PORT`,`NUTANIX_ENDPOINT`, `NUTANIX_STORAGE_CONTAINER` must be set for acceptance testing")
44+
t.Fatal("`NUTANIX_INSECURE`,`NUTANIX_PORT`,`NUTANIX_ENDPOINT`,`NUTANIX_STORAGE_CONTAINER` must be set for acceptance testing")
45+
}
46+
47+
// Check authentication - either username/password OR api_key must be set
48+
hasBasicAuth := os.Getenv("NUTANIX_USERNAME") != "" && os.Getenv("NUTANIX_PASSWORD") != ""
49+
hasAPIKey := os.Getenv("NUTANIX_API_KEY") != ""
50+
51+
if !hasBasicAuth && !hasAPIKey {
52+
t.Fatal("Either `NUTANIX_USERNAME` and `NUTANIX_PASSWORD`, or `NUTANIX_API_KEY` must be set for acceptance testing")
53+
}
54+
}
55+
56+
// TestAccPreCheckStorageContainer checks for storage container requirement
57+
// Use this in addition to TestAccPreCheck for tests that create VMs with disks
58+
func TestAccPreCheckStorageContainer(t *testing.T) {
59+
TestAccPreCheck(t)
60+
if os.Getenv("NUTANIX_STORAGE_CONTAINER") == "" {
61+
t.Fatal("`NUTANIX_STORAGE_CONTAINER` must be set for VM creation tests")
4662
}
4763
}
4864

nutanix/client/client.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ type Client struct {
5959
// RequestCompletionCallback defines the type of the request callback function
6060
type RequestCompletionCallback func(*http.Request, *http.Response, interface{})
6161

62-
// Credentials needed username and password
62+
// Credentials needed username and password (or API key)
6363
type Credentials struct {
6464
URL string
6565
Username string
@@ -75,6 +75,8 @@ type Credentials struct {
7575
NdbEndpoint string // Required field for connecting to Era VM APIs.
7676
NdbUsername string
7777
NdbPassword string
78+
APIKey string // API key for authentication (alternative to username/password)
79+
CustomHeaders map[string]string // Custom headers to add to all requests (e.g., for Cloudflare Access)
7880
}
7981

8082
// AdditionalFilter specification for client side filters
@@ -83,6 +85,31 @@ type AdditionalFilter struct {
8385
Values []string
8486
}
8587

88+
// applyAuthHeaders adds the appropriate authentication header to the request
89+
// Priority: Cookies (session auth) > API Key > Basic Auth
90+
func (c *Client) applyAuthHeaders(req *http.Request) {
91+
if c.Cookies != nil {
92+
// Session-based authentication using cookies
93+
for _, cookie := range c.Cookies {
94+
req.AddCookie(cookie)
95+
}
96+
} else if c.Credentials.APIKey != "" {
97+
// API key authentication
98+
req.Header.Add("X-Ntnx-Api-Key", c.Credentials.APIKey)
99+
} else {
100+
// Basic authentication (username/password)
101+
req.Header.Add("Authorization", "Basic "+
102+
base64.StdEncoding.EncodeToString([]byte(c.Credentials.Username+":"+c.Credentials.Password)))
103+
}
104+
}
105+
106+
// applyCustomHeaders adds any custom headers from credentials to the request
107+
func (c *Client) applyCustomHeaders(req *http.Request) {
108+
for key, value := range c.Credentials.CustomHeaders {
109+
req.Header.Add(key, value)
110+
}
111+
}
112+
86113
// NewClient returns a wrapper around http/https (as per isHTTP flag) client with additions of proxy & session_auth if given
87114
func NewClient(credentials *Credentials, userAgent string, absolutePath string, isHTTP bool) (*Client, error) {
88115
if userAgent == "" {
@@ -204,18 +231,12 @@ func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body int
204231
req.Header.Add("Content-Type", mediaType)
205232
req.Header.Add("Accept", mediaType)
206233
req.Header.Add("User-Agent", c.UserAgent)
207-
if c.Cookies != nil {
208-
for _, i := range c.Cookies {
209-
req.AddCookie(i)
210-
}
211-
} else {
212-
req.Header.Add("Authorization", "Basic "+
213-
base64.StdEncoding.EncodeToString([]byte(c.Credentials.Username+":"+c.Credentials.Password)))
214-
}
234+
c.applyAuthHeaders(req)
235+
c.applyCustomHeaders(req)
215236
return req, nil
216237
}
217238

218-
// NewRequest creates a request without authorisation headers
239+
// NewUnAuthRequest creates a request without authorisation headers
219240
func (c *Client) NewUnAuthRequest(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) {
220241
// check if client exists or not
221242
if c.client == nil {
@@ -312,13 +333,13 @@ func (c *Client) NewUploadRequest(ctx context.Context, method, urlStr string, fi
312333
req.Header.Add("Content-Type", octetStreamType)
313334
req.Header.Add("Accept", mediaType)
314335
req.Header.Add("User-Agent", c.UserAgent)
315-
req.Header.Add("Authorization", "Basic "+
316-
base64.StdEncoding.EncodeToString([]byte(c.Credentials.Username+":"+c.Credentials.Password)))
336+
c.applyAuthHeaders(req)
337+
c.applyCustomHeaders(req)
317338

318339
return req, nil
319340
}
320341

321-
// NewUploadRequest handles image uploads for image service
342+
// NewUnAuthUploadRequest handles image uploads for image service without auth
322343
func (c *Client) NewUnAuthUploadRequest(ctx context.Context, method, urlStr string, fileReader *os.File) (*http.Request, error) {
323344
// check if client exists or not
324345
if c.client == nil {

0 commit comments

Comments
 (0)