diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c6fd370..c5022ff4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/README.md b/README.md index 8769d55f2..0b3cd82bb 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 11f9c967f..194b06fe6 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -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 diff --git a/core/auth/auth.go b/core/auth/auth.go index 9330d49b7..19e400f94 100644 --- a/core/auth/auth.go +++ b/core/auth/auth.go @@ -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" ) @@ -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) } @@ -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 } @@ -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) } diff --git a/core/auth/auth_test.go b/core/auth/auth_test.go index 1464381fc..65647bd68 100644 --- a/core/auth/auth_test.go +++ b/core/auth/auth_test.go @@ -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() @@ -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 { @@ -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", "") } @@ -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", @@ -343,7 +423,7 @@ func TestDefaultAuth(t *testing.T) { { desc: "key_precedes_token", setToken: true, - setKeys: true, + setKeyPaths: true, isValid: true, expectedFlow: "key", }, @@ -352,10 +432,25 @@ 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 { @@ -363,12 +458,25 @@ func TestDefaultAuth(t *testing.T) { 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