Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Credential File support #200

Merged
merged 3 commits into from
Jul 31, 2023
Merged
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: 4 additions & 0 deletions .changelog/200.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
```release-note:feature
SDK can authenticate using a credential file. The credential file can specify
service principal credentials or workload identity provided credentials.
```
184 changes: 184 additions & 0 deletions auth/cred_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package auth

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"

"github.com/hashicorp/hcp-sdk-go/auth/workload"
)

const (
// EnvHCPCredFile is the environment variable that sets the HCP Credential
// File location.
EnvHCPCredFile = "HCP_CRED_FILE"

// CredentialFileName is the file name for the HCP credential file.
CredentialFileName = "cred_file.json"

// CredentialFileSchemeServicePrincipal is the credential file scheme value
// that indicates service principal credentials should be used to
// authenticate to HCP.
CredentialFileSchemeServicePrincipal = "service_principal_creds"

// CredentialFileSchemeWorkload is the credential file scheme value
// that indicates workload identity credentials should be used to
// authenticate to HCP.
CredentialFileSchemeWorkload = "workload"
)

var (
// testDefaultHCPCredFilePath is the the default HCP Credential File location during
// tests. The test should set its value.
testDefaultHCPCredFilePath = ""
)

// CredentialFile stores information required to authenticate to HCP APIs. It
// supports various authentication schemes, such as service principal
type CredentialFile struct {
// ProjectID captures the project ID of the service principal. It may be blank.
ProjectID string `json:"project_id"`

// Scheme is the authentication scheme. It may be one of: service_principal_creds, workload.
Scheme string `json:"scheme"`

// Workload configures the workload identity provider to exchange tokens
// with.
Workload *workload.IdentityProviderConfig `json:"workload"`

// Oauth configures authentication via Oauth.
Oauth *OauthConfig `json:"oauth"`
}

// OauthConfig configures authentication based on OAuth credentials.
type OauthConfig struct {
// ClientID is the client id of an HCP Service Principal
ClientID string `json:"client_id"`

// SecretID is the secret id of an HCP Service Principal
SecretID string `json:"secret_id"`
}

// Validate validates the CredentialFile
func (c *CredentialFile) Validate() error {
if c == nil {
return nil
}

if c.Scheme == CredentialFileSchemeServicePrincipal {
if c.Oauth == nil {
return fmt.Errorf("oauth config must be set when scheme is %q", CredentialFileSchemeServicePrincipal)
}

if err := c.Oauth.Validate(); err != nil {
return fmt.Errorf("oauth: %v", err)
}
} else if c.Scheme == CredentialFileSchemeWorkload {
if c.Workload == nil {
return fmt.Errorf("workload config must be set when scheme is %q", CredentialFileSchemeWorkload)
}

if err := c.Workload.Validate(); err != nil {
return fmt.Errorf("workload: %v", err)
}
} else {
return fmt.Errorf("scheme must be one of: %q, %q", CredentialFileSchemeServicePrincipal, CredentialFileSchemeWorkload)
}

if c.Workload != nil && c.Oauth != nil {
return fmt.Errorf("only one of oauth or workload may be set")
}

return nil
}

// Validate validates the OauthConfig
func (o *OauthConfig) Validate() error {
if o.ClientID == "" || o.SecretID == "" {
return fmt.Errorf("both client_id and secret_id must be set")
}

return nil
}

// ReadCredentialFile returns the credential file at the given path.
func ReadCredentialFile(path string) (*CredentialFile, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read credential file: %w", err)
}

var f CredentialFile
if err := json.Unmarshal(raw, &f); err != nil {
return nil, fmt.Errorf("failed to unmarshal credential file: %v", err)
}

return &f, f.Validate()

}

// GetDefaultCredentialFile returns the credential file by searching the default
// credential file location or by using the credential file environment variable
// to look for an override. If no credential file is found, a nil value will be
// returned with no error set.
func GetDefaultCredentialFile() (*CredentialFile, error) {
p, err := getCredentialFilePath()
if err != nil {
return nil, fmt.Errorf("failed to find credential file: %v", err)
}

// Read the credential file, but if no credential file is found, suppress
// the erorr.
cf, err := ReadCredentialFile(p)
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}

return cf, err
}

// getCredentialFilePath returns the credential file path, first looking for an
// overriding environment variable and then falling back to the default file
// location.
func getCredentialFilePath() (string, error) {
if testDefaultHCPCredFilePath != "" {
return testDefaultHCPCredFilePath, nil
}

if p, ok := os.LookupEnv(EnvHCPCredFile); ok {
return p, nil
}

// Get the user's home directory.
userHome, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to retrieve user's home directory path: %v", err)
}

p := filepath.Join(userHome, defaultDirectory, CredentialFileName)
return p, nil
}

// WriteDefaultCredentialFile writes the credential file to the default
// credential file location or to the value of EnvHCPCredFile if set.
func WriteDefaultCredentialFile(cf *CredentialFile) error {
p, err := getCredentialFilePath()
if err != nil {
return err
}

return WriteCredentialFile(p, cf)
}

// WriteCredentialFile writes the given credential file to the path.
func WriteCredentialFile(path string, cf *CredentialFile) error {
data, err := json.MarshalIndent(cf, "", " ")
if err != nil {
return err
}

return ioutil.WriteFile(path, data, directoryPermissions)
}
Loading