Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Release (2025-XX-XX)

- `core`: [v0.16.1](core/CHANGELOG.md#v0161-2025-02-25)
- **Bugfix:** STACKIT_PRIVATE_KEY and STACKIT_SERVICE_ACCOUNT_KEY can be set via environment variable or via credentials file.
- `stackitmarketplace`: [v0.3.0](services/stackitmarketplace/CHANGELOG.md#v030-2025-02-25)
- **Feature:** Add method to create inquiries: `InquiriesCreateInquiry`
- **Feature:** Add `sort` property to `ApiListCatalogProductsRequest`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ The SDK supports two authentication methods:
The SDK searches for credentials in the following order:

1. Explicit configuration in code
2. Environment variables
2. Environment variables (KEY_PATH for KEY)
3. Credentials file (`$HOME/.stackit/credentials.json`)

For each authentication method, the key flow is attempted first, followed by the token flow.
Expand Down
4 changes: 4 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## v0.16.1 (2025-02-25)

- **Bugfix:** STACKIT_PRIVATE_KEY and STACKIT_SERVICE_ACCOUNT_KEY can be set via environment variable or via credentials file.

## v0.16.0 (2025-02-21)
- **New:** Minimal go version is now Go 1.21

Expand Down
43 changes: 36 additions & 7 deletions core/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ type Credentials struct {
STACKIT_SERVICE_ACCOUNT_TOKEN string
STACKIT_SERVICE_ACCOUNT_KEY_PATH string
STACKIT_PRIVATE_KEY_PATH string
STACKIT_SERVICE_ACCOUNT_KEY string
STACKIT_PRIVATE_KEY string
}

const (
credentialsFilePath = ".stackit/credentials.json" //nolint:gosec // linter false positive
tokenCredentialType credentialType = "token"
serviceAccountKeyCredentialType credentialType = "service_account_key"
serviceAccountKeyPathCredentialType credentialType = "service_account_key_path"
privateKeyCredentialType credentialType = "private_key"
privateKeyPathCredentialType credentialType = "private_key_path"
)

Expand Down Expand Up @@ -239,6 +243,16 @@ func readCredential(cred credentialType, credentials *Credentials) (string, erro
if credentialValue == "" {
return credentialValue, fmt.Errorf("private key path is empty or not set")
}
case serviceAccountKeyCredentialType:
credentialValue = credentials.STACKIT_SERVICE_ACCOUNT_KEY
if credentialValue == "" {
return credentialValue, fmt.Errorf("service account key is empty or not set")
}
case privateKeyCredentialType:
credentialValue = credentials.STACKIT_PRIVATE_KEY
if credentialValue == "" {
return credentialValue, fmt.Errorf("private key is empty or not set")
}
default:
return "", fmt.Errorf("invalid credential type: %s", cred)
}
Expand Down Expand Up @@ -268,21 +282,36 @@ func getServiceAccountEmail(cfg *config.Configuration) string {
}

// getKey searches for a key in the following order: client configuration, environment variable, credentials file.
func getKey(cfgKey, cfgKeyPath *string, envVar, credType credentialType, cfgCredFilePath string) error {
func getKey(cfgKey, cfgKeyPath *string, envVarKeyPath, envVarKey string, credTypePath, credTypeKey credentialType, cfgCredFilePath string) error {
if *cfgKey != "" {
return nil
}
if *cfgKeyPath == "" {
keyPath, keyPathSet := os.LookupEnv(string(envVar))
if !keyPathSet || keyPath == "" {
// check environment variable: path
keyPath, keyPathSet := os.LookupEnv(envVarKeyPath)
// check environment variable: key
key, keySet := os.LookupEnv(envVarKey)
// if both are not set -> read from credentials file
if (!keyPathSet || keyPath == "") && (!keySet || key == "") {
credentials, err := readCredentialsFile(cfgCredFilePath)
if err != nil {
return fmt.Errorf("reading from credentials file: %w", err)
}
keyPath, err = readCredential(credType, credentials)
// read key path from credentials file
keyPath, err = readCredential(credTypePath, credentials)
if err != nil || keyPath == "" {
return fmt.Errorf("neither key nor path is provided in the configuration, environment variable, or credentials file: %w", err)
// key path was not provided, read key from credentials file
key, err = readCredential(credTypeKey, credentials)
if err != nil || key == "" {
return fmt.Errorf("neither key nor path is provided in the configuration, environment variable, or credentials file: %w", err)
}
*cfgKey = key
return nil
}
} else if !keyPathSet || keyPath == "" {
// key path was not provided, use key
*cfgKey = key
return nil
}
*cfgKeyPath = keyPath
}
Expand All @@ -299,10 +328,10 @@ func getKey(cfgKey, cfgKeyPath *string, envVar, credType credentialType, cfgCred

// getServiceAccountKey configures the service account key in the provided configuration
func getServiceAccountKey(cfg *config.Configuration) error {
return getKey(&cfg.ServiceAccountKey, &cfg.ServiceAccountKeyPath, "STACKIT_SERVICE_ACCOUNT_KEY_PATH", serviceAccountKeyPathCredentialType, cfg.CredentialsFilePath)
return getKey(&cfg.ServiceAccountKey, &cfg.ServiceAccountKeyPath, "STACKIT_SERVICE_ACCOUNT_KEY_PATH", "STACKIT_SERVICE_ACCOUNT_KEY", serviceAccountKeyPathCredentialType, serviceAccountKeyCredentialType, cfg.CredentialsFilePath)
}

// getPrivateKey configures the private key in the provided configuration
func getPrivateKey(cfg *config.Configuration) error {
return getKey(&cfg.PrivateKey, &cfg.PrivateKeyPath, "STACKIT_PRIVATE_KEY_PATH", privateKeyPathCredentialType, cfg.CredentialsFilePath)
return getKey(&cfg.PrivateKey, &cfg.PrivateKeyPath, "STACKIT_PRIVATE_KEY_PATH", "STACKIT_PRIVATE_KEY", privateKeyPathCredentialType, privateKeyCredentialType, cfg.CredentialsFilePath)
}
180 changes: 144 additions & 36 deletions core/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ func fixtureServiceAccountKey(mods ...func(*clients.ServiceAccountKeyResponse))
return serviceAccountKeyResponse
}

// helper function to create a credentials.json file with saKey and private key
func createCredentialsKeyJson(serviceAccountKey, privateKey string) ([]byte, error) {
tempMap := map[string]interface{}{}
tempMap["STACKIT_SERVICE_ACCOUNT_KEY"] = serviceAccountKey
tempMap["STACKIT_PRIVATE_KEY"] = privateKey
return json.Marshal(tempMap)
}

// Error cases are tested in the NoAuth, KeyAuth, TokenAuth and DefaultAuth functions
func TestSetupAuth(t *testing.T) {
privateKey, err := generatePrivateKey()
Expand Down Expand Up @@ -111,57 +119,104 @@ func TestSetupAuth(t *testing.T) {
}
}()

// create a credentials file with saKey and private key
credentialsKeyFile, errs := os.CreateTemp("", "temp-*.txt")
if errs != nil {
t.Fatalf("Creating temporary file: %s", err)
}
defer func() {
err := os.Remove(credentialsKeyFile.Name())
if err != nil {
t.Fatalf("Removing temporary file: %s", err)
}
}()

credKeyJson, err := createCredentialsKeyJson(string(saKey), privateKey)
if err != nil {
t.Fatalf("createCredentialsKeyJson: %s", err)
}
_, errs = credentialsKeyFile.WriteString(string(credKeyJson))
if errs != nil {
t.Fatalf("Writing credentials json to temporary file: %s", err)
}

for _, test := range []struct {
desc string
config *config.Configuration
setToken bool
setKeys bool
setPath bool
isValid bool
desc string
config *config.Configuration
setToken bool
setKeys bool
setKeyPaths bool
setCredentialsFilePathToken bool
setCredentialsFilePathKey bool
isValid bool
}{
{
desc: "token_config",
config: nil,
setToken: true,
setPath: false,
isValid: true,
desc: "token_config",
config: nil,
setToken: true,
setCredentialsFilePathToken: false,
isValid: true,
},
{
desc: "key_config",
config: nil,
setKeys: true,
setPath: false,
isValid: true,
desc: "key_config",
config: nil,
setKeys: true,
setCredentialsFilePathToken: false,
isValid: true,
},
{
desc: "valid_path_to_file",
config: nil,
setToken: false,
setPath: true,
isValid: true,
desc: "key_config_path",
config: nil,
setKeys: false,
setKeyPaths: true,
setCredentialsFilePathToken: false,
isValid: true,
},
{
desc: "key_config_credentials_path",
config: nil,
setKeys: false,
setKeyPaths: false,
setCredentialsFilePathKey: true,
isValid: true,
},
{
desc: "valid_path_to_file",
config: nil,
setToken: false,
setCredentialsFilePathToken: true,
isValid: true,
},
{
desc: "custom_config_token",
config: &config.Configuration{
Token: "token",
},
setToken: false,
setPath: false,
isValid: true,
setToken: false,
setCredentialsFilePathToken: false,
isValid: true,
},
{
desc: "custom_config_path",
config: &config.Configuration{
CredentialsFilePath: "test_resources/test_credentials_bar.json",
},
setToken: false,
setPath: false,
isValid: true,
setToken: false,
setCredentialsFilePathToken: false,
isValid: true,
},
} {
t.Run(test.desc, func(t *testing.T) {
setTemporaryHome(t)
if test.setKeys {
t.Setenv("STACKIT_SERVICE_ACCOUNT_KEY", string(saKey))
t.Setenv("STACKIT_PRIVATE_KEY", privateKey)
} else {
t.Setenv("STACKIT_SERVICE_ACCOUNT_KEY", "")
t.Setenv("STACKIT_PRIVATE_KEY", "")
}

if test.setKeyPaths {
t.Setenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH", saKeyFile.Name())
t.Setenv("STACKIT_PRIVATE_KEY_PATH", privateKeyFile.Name())
} else {
Expand All @@ -175,8 +230,10 @@ func TestSetupAuth(t *testing.T) {
t.Setenv("STACKIT_SERVICE_ACCOUNT_TOKEN", "")
}

if test.setPath {
if test.setCredentialsFilePathToken {
t.Setenv("STACKIT_CREDENTIALS_PATH", "test_resources/test_credentials_bar.json")
} else if test.setCredentialsFilePathKey {
t.Setenv("STACKIT_CREDENTIALS_PATH", credentialsKeyFile.Name())
} else {
t.Setenv("STACKIT_CREDENTIALS_PATH", "")
}
Expand Down Expand Up @@ -327,12 +384,35 @@ func TestDefaultAuth(t *testing.T) {
}
}()

// create a credentials file with saKey and private key
credentialsKeyFile, errs := os.CreateTemp("", "temp-*.txt")
if errs != nil {
t.Fatalf("Creating temporary file: %s", err)
}
defer func() {
err := os.Remove(credentialsKeyFile.Name())
if err != nil {
t.Fatalf("Removing temporary file: %s", err)
}
}()

credKeyJson, err := createCredentialsKeyJson(string(saKey), privateKey)
if err != nil {
t.Fatalf("createCredentialsKeyJson: %s", err)
}
_, errs = credentialsKeyFile.WriteString(string(credKeyJson))
if errs != nil {
t.Fatalf("Writing credentials json to temporary file: %s", err)
}

for _, test := range []struct {
desc string
setToken bool
setKeys bool
isValid bool
expectedFlow string
desc string
setToken bool
setKeyPaths bool
setKeys bool
setCredentialsFilePathKey bool
isValid bool
expectedFlow string
}{
{
desc: "token",
Expand All @@ -343,7 +423,7 @@ func TestDefaultAuth(t *testing.T) {
{
desc: "key_precedes_token",
setToken: true,
setKeys: true,
setKeyPaths: true,
isValid: true,
expectedFlow: "key",
},
Expand All @@ -352,23 +432,51 @@ func TestDefaultAuth(t *testing.T) {
setToken: false,
isValid: false,
},
{
desc: "use keys via environment",
setKeys: true,
setToken: false,
isValid: true,
expectedFlow: "key",
},
{
desc: "use keys via credentials file",
setKeys: false,
setToken: false,
setCredentialsFilePathKey: true,
isValid: true,
expectedFlow: "key",
},
} {
t.Run(test.desc, func(t *testing.T) {
setTemporaryHome(t)
if test.setKeys {
if test.setKeyPaths {
t.Setenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH", saKeyFile.Name())
t.Setenv("STACKIT_PRIVATE_KEY_PATH", privateKeyFile.Name())
} else {
t.Setenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH", "")
t.Setenv("STACKIT_PRIVATE_KEY_PATH", "")
}

if test.setKeys {
t.Setenv("STACKIT_SERVICE_ACCOUNT_KEY", string(saKey))
t.Setenv("STACKIT_PRIVATE_KEY", privateKey)
} else {
t.Setenv("STACKIT_SERVICE_ACCOUNT_KEY", "")
t.Setenv("STACKIT_PRIVATE_KEY", "")
}

if test.setCredentialsFilePathKey {
t.Setenv("STACKIT_CREDENTIALS_PATH", credentialsKeyFile.Name())
} else {
t.Setenv("STACKIT_CREDENTIALS_PATH", "test-path")
}

if test.setToken {
t.Setenv("STACKIT_SERVICE_ACCOUNT_TOKEN", "test-token")
} else {
t.Setenv("STACKIT_SERVICE_ACCOUNT_TOKEN", "")
}
t.Setenv("STACKIT_CREDENTIALS_PATH", "test-path")
t.Setenv("STACKIT_SERVICE_ACCOUNT_EMAIL", "test-email")

// Get the default authentication client and ensure that it's not nil
Expand Down
Loading