Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# 2.4.0 (2025-**-**)
# 2.4.0

- Configured SSL certificate based authentication in addition to basic username and password authentication in provider config.

# 2.3.0 (2025-07-22)

Expand Down
8 changes: 8 additions & 0 deletions examples/provider/provider.tf
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ provider "netapp-ontap" {
username = var.username
password = var.password
validate_certs = var.validate_certs
},
{
name = "cluster6"
hostname = "********82"
cert_filepath = var.cert_filepath
key_filepath = var.key_filepath
ca_cert_file = var.ca_cert_file
validate_certs = var.validate_certs
}
]
}
14 changes: 11 additions & 3 deletions examples/provider/terraform.tfvars
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
username = "admin"
password = "xxxxxxxxx"
validate_certs = true
# Basic username password authentication

username = "tfuser"
password = "netapp1!"
validate_certs = false

# cert based authentication

# cert_filepath = "/root/cert/terraform_client.crt"
# key_filepath = "/root/cert/terraform_client.key"
# ca_cert_file = "/root/cert/rootCA.crt"
15 changes: 15 additions & 0 deletions examples/provider/variables.tf
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
# Terraform will prompt for values, unless a tfvars file is present.
variable "username" {
type = string
default = null
}
variable "password" {
type = string
sensitive = true
default = null
}
variable "validate_certs" {
type = bool
default = false
}
variable "client_cert_file" {
type = string
default = null
}
variable "client_key_file" {
type = string
default = null
}
variable "ca_cert_file" {
type = string
default = null
}
3 changes: 3 additions & 0 deletions internal/provider/connection/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type Profile struct {
MaxConcurrentRequests int
UseAWSLambda bool
AWS AWSConfig `mapstructure:"aws,omitempty"`
CertFilepath string
KeyFilepath string
CACertFile string
}

type AWSConfig struct {
Expand Down
22 changes: 20 additions & 2 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type ConnectionProfileModel struct {
Password types.String `tfsdk:"password"`
ValidateCerts types.Bool `tfsdk:"validate_certs"`
ONTAPProviderAWSModel types.Object `tfsdk:"aws_lambda"`
CertFilepath types.String `tfsdk:"cert_filepath"`
KeyFilepath types.String `tfsdk:"key_filepath"`
CACertFile types.String `tfsdk:"ca_cert_file"`
}

// ONTAPProviderModel describes the provider data model.
Expand Down Expand Up @@ -94,17 +97,29 @@ func (p *ONTAPProvider) Schema(ctx context.Context, req provider.SchemaRequest,
},
"username": schema.StringAttribute{
MarkdownDescription: "ONTAP management user name (cluster or svm)",
Required: true,
Optional: true,
},
"password": schema.StringAttribute{
MarkdownDescription: "ONTAP management password for username",
Required: true,
Optional: true,
Sensitive: true,
},
"validate_certs": schema.BoolAttribute{
MarkdownDescription: "Whether to enforce SSL certificate validation, defaults to true. Not applicable for AWS Lambda",
Optional: true,
},
"key_filepath": schema.StringAttribute{
MarkdownDescription: "Path to the key file. Not applicable for AWS Lambda",
Optional: true,
},
"cert_filepath": schema.StringAttribute{
MarkdownDescription: "Path to the certificate file. Not applicable for AWS Lambda",
Optional: true,
},
"ca_cert_file": schema.StringAttribute{
MarkdownDescription: "Path to the CA certificate file. Not applicable for AWS Lambda",
Optional: true,
},
"aws_lambda": schema.SingleNestedAttribute{
MarkdownDescription: "AWS configuration for Lambda",
Optional: true,
Expand Down Expand Up @@ -178,6 +193,9 @@ func (p *ONTAPProvider) Configure(ctx context.Context, req provider.ConfigureReq
Password: connectionProfile.Password.ValueString(),
ValidateCerts: validateCerts,
MaxConcurrentRequests: 0,
CertFilepath: connectionProfile.CertFilepath.ValueString(),
KeyFilepath: connectionProfile.KeyFilepath.ValueString(),
CACertFile: connectionProfile.CACertFile.ValueString(),
}
if !connectionProfile.ONTAPProviderAWSModel.IsNull() {
var lambdaConfig ONTAPProviderAWSLambdaModel
Expand Down
115 changes: 111 additions & 4 deletions internal/restclient/httpclient/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package httpclient
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"net/http"
"os"
"time"

"github.com/hashicorp/terraform-plugin-log/tflog"
Expand All @@ -26,6 +29,47 @@ type HTTPProfile struct {
Username string
Password string
ValidateCerts bool
CertFilepath string
KeyFilepath string
CACertFile string
}

// AuthMethod represents the authentication method to use
type AuthMethod int

const (
AuthMethodNone AuthMethod = iota
AuthMethodBasic
AuthMethodSingleCert // single_cert - cert file only
AuthMethodCertKey // cert_key - cert and key files
)

// setAuthMethod determines the authentication method based on available credentials
func (c *HTTPClient) setAuthMethod() (AuthMethod, error) {
// cert authentication is prioritized if both basic and client certificate authentication parameters are given
if c.cxProfile.CertFilepath != "" {
if c.cxProfile.KeyFilepath == "" {
// single_cert method (cert file only, no key file)
return AuthMethodSingleCert, nil
} else {
// cert_key method (both cert and key files)
return AuthMethodCertKey, nil
}
} else {
// No certificate file provided
if c.cxProfile.Password == "" && c.cxProfile.Username == "" {
if c.cxProfile.KeyFilepath != "" {
return AuthMethodNone, errors.New("cannot have a key file without a cert file")
} else {
return AuthMethodNone, errors.New("ONTAP module requires username/password or SSL certificate file(s)")
}
} else if c.cxProfile.Password != "" && c.cxProfile.Username != "" {
// Both username and password provided - use basic auth
return AuthMethodBasic, nil
} else {
return AuthMethodNone, errors.New("username and password have to be provided together")
}
}
}

// Do sends the API Request, parses the response as JSON, and returns the HTTP status code as int, the "result" value as byte
Expand Down Expand Up @@ -80,10 +124,73 @@ func NewClient(ctx context.Context, cxProfile HTTPProfile, tag string) HTTPClien
return client
}

// create configures and creates the http client
func (c HTTPClient) create() http.Client {
if !c.cxProfile.ValidateCerts {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
tlsConfig := &tls.Config{
InsecureSkipVerify: !c.cxProfile.ValidateCerts,
// When using client certificates, we might want to skip server cert validation
// but still validate client certs
}

// Determine and log authentication method
authMethod, err := c.setAuthMethod()
if err != nil {
tflog.Error(c.ctx, fmt.Sprintf("Authentication configuration error: %v", err))
} else {
var authDesc string
switch authMethod {
case AuthMethodBasic:
authDesc = "basic_auth (username/password)"
tflog.Debug(c.ctx, "Using basic authentication")
case AuthMethodSingleCert:
authDesc = "single_cert (certificate only)"
tflog.Debug(c.ctx, "Using single certificate authentication")
case AuthMethodCertKey:
authDesc = "cert_key (certificate with key)"
tflog.Debug(c.ctx, "Using certificate key authentication")
default:
authDesc = "none"
}
tflog.Debug(c.ctx, fmt.Sprintf("Using authentication method: %s", authDesc))
}

// Load client cert/key if provided (for cert-based auth)
if c.cxProfile.CertFilepath != "" {
if c.cxProfile.KeyFilepath != "" {
// cert_key method - load both cert and key
cert, err := tls.LoadX509KeyPair(c.cxProfile.CertFilepath, c.cxProfile.KeyFilepath)
if err != nil {
tflog.Error(c.ctx, fmt.Sprintf("Failed to load client certificate: %v", err))
} else {
tlsConfig.Certificates = []tls.Certificate{cert}
tflog.Debug(c.ctx, "Client certificate and key loaded successfully")
}
} else {
// single_cert method - load only certificate (no private key)
// This is typically used for server certificate validation, not client auth
tflog.Debug(c.ctx, "Single certificate mode - certificate file provided without key")
}
}

// Load CA cert if provided
if c.cxProfile.CACertFile != "" {
caCertPool := x509.NewCertPool()
caCert, err := os.ReadFile(c.cxProfile.CACertFile)
if err != nil {
tflog.Error(c.ctx, fmt.Sprintf("Failed to load CA certificate: %v", err))
} else {
if caCertPool.AppendCertsFromPEM(caCert) {
tlsConfig.RootCAs = caCertPool
tflog.Debug(c.ctx, "CA certificate loaded successfully")
} else {
tflog.Error(c.ctx, "Failed to parse CA certificate")
}
}
}

return http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
Timeout: 120 * time.Second,
}
return http.Client{Timeout: 120 * time.Second}
}
20 changes: 19 additions & 1 deletion internal/restclient/httpclient/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
Expand Down Expand Up @@ -40,7 +41,24 @@ func (r *Request) BuildHTTPReq(c *HTTPClient, baseURL string) (*http.Request, er
}

req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(c.cxProfile.Username, c.cxProfile.Password)

authMethod, err := c.setAuthMethod()
if err != nil {
return nil, fmt.Errorf("authentication error: %v", err)
}

// Apply authentication based on the determined method
switch authMethod {
case AuthMethodBasic:
// basic_auth - use username and password
req.SetBasicAuth(c.cxProfile.Username, c.cxProfile.Password)
case AuthMethodSingleCert:
// single_cert - certificate authentication only (no Basic Auth headers)
// Certificate authentication is handled at TLS level in http.Client
case AuthMethodCertKey:
// cert_key - certificate with key authentication (no Basic Auth headers)
// Certificate authentication is handled at TLS level in http.Client
}
// telemetry header
req.Header.Set("X-Dot-Client-App", c.tag)
// TODO: low pty: add support for form data (require to create a file)
Expand Down
6 changes: 6 additions & 0 deletions internal/restclient/rest_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ type ConnectionProfile struct {
MaxConcurrentRequests int
UseAWSLambda bool
AWS AWSConfig `mapstructure:"AWS,omitempty"`
CertFilepath string
KeyFilepath string
CACertFile string
}

type AWSConfig struct {
Expand Down Expand Up @@ -219,6 +222,9 @@ func NewClient(ctx context.Context, cxProfile ConnectionProfile, tag string, job
return nil, errors.New(msg)
}
httpProfile.APIRoot = "api"
httpProfile.CertFilepath = cxProfile.CertFilepath
httpProfile.KeyFilepath = cxProfile.KeyFilepath
httpProfile.CACertFile = cxProfile.CACertFile
maxConcurrentRequests := cxProfile.MaxConcurrentRequests
if maxConcurrentRequests == 0 {
maxConcurrentRequests = 6
Expand Down
Loading