Skip to content

Commit 804fbab

Browse files
committed
fix: Support reading STACKIT_SERVICE_CONNECTION_ID on Azure Pipelines
Signed-off-by: Jorge Turrado <jorge_turrado@hotmail.es>
1 parent da09800 commit 804fbab

3 files changed

Lines changed: 63 additions & 2 deletions

File tree

‎AUTHENTICATION.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,5 @@ Using this flow is less secure since the token is long-lived. You can provide th
131131
# Optional: provide a file path containing the OIDC token
132132
STACKIT_FEDERATED_TOKEN_FILE=/path/to/oidc-token.jwt
133133
```
134+
135+
When running in Azure DevOps with the [STACKIT Service Connection](https://marketplace.visualstudio.com/items?itemName=SchwarzDigits.stackit-service-connection), the CLI reads `STACKIT_SERVICE_CONNECTION_ID` and requests the OIDC token for that service connection.

‎internal/pkg/auth/oidc.go‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const (
1717
EnvGitHubRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN" //nolint:gosec // linter false positive
1818
EnvAzureOIDCRequestURI = "SYSTEM_OIDCREQUESTURI"
1919
EnvAzureAccessToken = "SYSTEM_ACCESSTOKEN" //nolint:gosec // linter false positive
20+
EnvAzureServiceConnectionID = "STACKIT_SERVICE_CONNECTION_ID"
2021
)
2122

2223
func IsOIDCEnabled() bool {
@@ -40,7 +41,8 @@ func OIDCServiceAccountEmail() string {
4041
// TokenFunc returns the OIDCTokenFunc to use for Workload Identity Federation.
4142
// It checks the following token sources in order: STACKIT_SERVICE_ACCOUNT_FEDERATED_TOKEN,
4243
// STACKIT_FEDERATED_TOKEN_FILE, GitHub Actions (ACTIONS_ID_TOKEN_REQUEST_URL +
43-
// ACTIONS_ID_TOKEN_REQUEST_TOKEN), and Azure DevOps (SYSTEM_OIDCREQUESTURI + SYSTEM_ACCESSTOKEN).
44+
// ACTIONS_ID_TOKEN_REQUEST_TOKEN), and Azure DevOps (SYSTEM_OIDCREQUESTURI + SYSTEM_ACCESSTOKEN,
45+
// optionally scoped to the service connection set in STACKIT_SERVICE_CONNECTION_ID).
4446
// Returns an error if no source is detected.
4547
func OIDCTokenFunc() (oidcadapters.OIDCTokenFunc, error) {
4648
// static token provided directly via env var
@@ -65,7 +67,7 @@ func OIDCTokenFunc() (oidcadapters.OIDCTokenFunc, error) {
6567
// Azure DevOps
6668
if adoURL := os.Getenv(EnvAzureOIDCRequestURI); adoURL != "" {
6769
if adoToken := os.Getenv(EnvAzureAccessToken); adoToken != "" {
68-
return oidcadapters.RequestAzureDevOpsOIDCToken(adoURL, adoToken, ""), nil
70+
return oidcadapters.RequestAzureDevOpsOIDCToken(adoURL, adoToken, os.Getenv(EnvAzureServiceConnectionID)), nil
6971
}
7072
}
7173

‎internal/pkg/auth/oidc_test.go‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"net/http"
77
"net/http/httptest"
8+
"net/url"
89
"testing"
910

1011
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
@@ -169,6 +170,62 @@ func TestTokenFunc_AzureDevOps(t *testing.T) {
169170
}
170171
}
171172

173+
func TestTokenFunc_AzureDevOps_ServiceConnectionID(t *testing.T) {
174+
tests := []struct {
175+
description string
176+
serviceConnectionID string
177+
}{
178+
{
179+
description: "service connection id set",
180+
serviceConnectionID: "my-service-connection-id",
181+
},
182+
{
183+
description: "service connection id unset",
184+
serviceConnectionID: "",
185+
},
186+
}
187+
188+
for _, tt := range tests {
189+
t.Run(tt.description, func(t *testing.T) {
190+
var gotQuery url.Values
191+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
192+
gotQuery = r.URL.Query()
193+
w.Header().Set("Content-Type", "application/json")
194+
tok := "ado-oidc-token"
195+
_ = json.NewEncoder(w).Encode(map[string]*string{"oidcToken": &tok})
196+
}))
197+
defer srv.Close()
198+
199+
t.Setenv(auth.EnvServiceAccountFederatedToken, "")
200+
t.Setenv(auth.EnvFederatedTokenFile, "")
201+
t.Setenv(auth.EnvGitHubRequestURL, "")
202+
t.Setenv(auth.EnvGitHubRequestToken, "")
203+
t.Setenv(auth.EnvAzureOIDCRequestURI, srv.URL)
204+
t.Setenv(auth.EnvAzureAccessToken, "ado-access-token")
205+
t.Setenv(auth.EnvAzureServiceConnectionID, tt.serviceConnectionID)
206+
207+
fn, err := auth.OIDCTokenFunc()
208+
if err != nil {
209+
t.Fatalf("OIDCTokenFunc() unexpected error: %v", err)
210+
}
211+
if _, err := fn(context.Background()); err != nil {
212+
t.Fatalf("fn() unexpected error: %v", err)
213+
}
214+
215+
got, ok := gotQuery["serviceConnectionId"]
216+
if tt.serviceConnectionID == "" {
217+
if ok {
218+
t.Errorf("serviceConnectionId query param = %q, want it absent", got)
219+
}
220+
return
221+
}
222+
if !ok || len(got) != 1 || got[0] != tt.serviceConnectionID {
223+
t.Errorf("serviceConnectionId query param = %q, want %q", got, tt.serviceConnectionID)
224+
}
225+
})
226+
}
227+
}
228+
172229
func TestTokenFunc_NoSource(t *testing.T) {
173230
// All env vars absent → must return an actionable error, no panic.
174231
t.Setenv(auth.EnvServiceAccountFederatedToken, "")

0 commit comments

Comments
 (0)