Add AzurePipelinesCredential for service connection authentication (#22891)

This commit is contained in:
Charles Lowell 2024-05-13 15:56:41 -07:00 коммит произвёл GitHub
Родитель cef4ac10f7
Коммит 4fd61cce02
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
5 изменённых файлов: 200 добавлений и 7 удалений

Просмотреть файл

@ -1,14 +1,10 @@
# Release History
## 1.6.0-beta.4 (Unreleased)
## 1.6.0-beta.4 (2024-05-14)
### Features Added
### Breaking Changes
### Bugs Fixed
### Other Changes
* `AzurePipelinesCredential` authenticates an Azure Pipeline service connection with
workload identity federation
## 1.6.0-beta.3 (2024-04-09)

Просмотреть файл

@ -48,6 +48,7 @@ const (
var (
accessTokenRespSuccess = []byte(fmt.Sprintf(`{"access_token": "%s","expires_in": %d,"token_type":"Bearer"}`, tokenValue, tokenExpiresIn))
ctx = context.Background()
testTRO = policy.TokenRequestOptions{Scopes: []string{liveTestScope}}
)

Просмотреть файл

@ -0,0 +1,124 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
)
const (
credNameAzurePipelines = "AzurePipelinesCredential"
oidcAPIVersion = "7.1"
systemAccessToken = "SYSTEM_ACCESSTOKEN"
systemOIDCRequestURI = "SYSTEM_OIDCREQUESTURI"
)
// AzurePipelinesCredential authenticates with workload identity federation in an Azure Pipeline. See
// [Azure Pipelines documentation] for more information.
//
// [Azure Pipelines documentation]: https://learn.microsoft.com/azure/devops/pipelines/library/connect-to-azure?view=azure-devops#create-an-azure-resource-manager-service-connection-that-uses-workload-identity-federation
type AzurePipelinesCredential struct {
connectionID, oidcURI, systemAccessToken string
cred *ClientAssertionCredential
}
// AzurePipelinesServiceConnectionCredentialOptions contains optional parameters for AzurePipelinesServiceConnectionCredential.
type AzurePipelinesServiceConnectionCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
}
// NewAzurePipelinesCredential is the constructor for AzurePipelinesCredential. In addition to its required arguments,
// it reads a security token for the running build, which is required to authenticate the service connection, from the
// environment variable SYSTEM_ACCESSTOKEN. See the [Azure Pipelines documentation] for an example showing how to set
// this variable in build job YAML.
//
// [Azure Pipelines documentation]: https://learn.microsoft.com/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken
func NewAzurePipelinesCredential(tenantID, clientID, serviceConnectionID string, options *AzurePipelinesServiceConnectionCredentialOptions) (*AzurePipelinesCredential, error) {
if options == nil {
options = &AzurePipelinesServiceConnectionCredentialOptions{}
}
u := os.Getenv(systemOIDCRequestURI)
if u == "" {
return nil, fmt.Errorf("no value for environment variable %s. This should be set by Azure Pipelines", systemOIDCRequestURI)
}
sat := os.Getenv(systemAccessToken)
if sat == "" {
return nil, errors.New("no value for environment variable " + systemAccessToken)
}
a := AzurePipelinesCredential{
connectionID: serviceConnectionID,
oidcURI: u,
systemAccessToken: sat,
}
caco := ClientAssertionCredentialOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
cred, err := NewClientAssertionCredential(tenantID, clientID, a.getAssertion, &caco)
if err != nil {
return nil, err
}
cred.client.name = credNameAzurePipelines
a.cred = cred
return &a, nil
}
// GetToken requests an access token from Microsoft Entra ID. Azure SDK clients call this method automatically.
func (a *AzurePipelinesCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameAzurePipelines+"."+traceOpGetToken, a.cred.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := a.cred.GetToken(ctx, opts)
return tk, err
}
func (a *AzurePipelinesCredential) getAssertion(ctx context.Context) (string, error) {
url := a.oidcURI + "?api-version=" + oidcAPIVersion + "&serviceConnectionId=" + a.connectionID
url, err := runtime.EncodeQueryParams(url)
if err != nil {
return "", err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+a.systemAccessToken)
res, err := doForClient(a.cred.client.azClient, req)
if err != nil {
return "", err
}
b, err := runtime.Payload(res)
if err != nil {
return "", err
}
var r struct {
OIDCToken string `json:"oidcToken"`
}
err = json.Unmarshal(b, &r)
if err != nil {
return "", err
}
return r.OIDCToken, nil
}

Просмотреть файл

@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"fmt"
"net/http"
"net/url"
"os"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/internal/mock"
"github.com/Azure/azure-sdk-for-go/sdk/internal/recording"
"github.com/stretchr/testify/require"
)
func TestAzurePipelinesCredential(t *testing.T) {
t.Run("getAssertion", func(t *testing.T) {
srv, close := mock.NewServer()
defer close()
t.Setenv(systemOIDCRequestURI, srv.URL())
oidcAccessToken := "token"
t.Setenv(systemAccessToken, oidcAccessToken)
connectionID := "connection"
expected, err := url.Parse(fmt.Sprintf(
"%s/?api-version=%s&serviceConnectionId=%s",
srv.URL(), oidcAPIVersion, connectionID,
))
require.NoError(t, err, "test bug: expected URL should parse")
srv.AppendResponse(
mock.WithBody([]byte(fmt.Sprintf(`{"oidcToken":%q}`, oidcAccessToken))),
mock.WithPredicate(func(r *http.Request) bool {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, expected.Host, r.Host)
require.Equal(t, expected.Path, r.URL.Path)
require.Equal(t, expected.RawQuery, r.URL.RawQuery)
return true
}),
)
srv.AppendResponse()
o := AzurePipelinesServiceConnectionCredentialOptions{
ClientOptions: azcore.ClientOptions{
Transport: srv,
},
}
cred, err := NewAzurePipelinesCredential(fakeTenantID, fakeClientID, connectionID, &o)
require.NoError(t, err)
actual, err := cred.getAssertion(ctx)
require.NoError(t, err)
require.Equal(t, oidcAccessToken, actual)
})
t.Run("Live", func(t *testing.T) {
if recording.GetRecordMode() != recording.LiveMode {
t.Skip("this test runs only live in an Azure Pipeline with a configured service connection")
}
clientID := os.Getenv("AZURE_SERVICE_CONNECTION_CLIENT_ID")
connectionID := os.Getenv("AZURE_SERVICE_CONNECTION_ID")
tenantID := os.Getenv("AZURE_SERVICE_CONNECTION_TENANT_ID")
for _, s := range []string{clientID, connectionID, tenantID} {
if s == "" {
t.Skip("set AZURE_SERVICE_CONNECTION_CLIENT_ID, AZURE_SERVICE_CONNECTION_ID and AZURE_SERVICE_CONNECTION_TENANT_ID to run this test")
}
}
cred, err := NewAzurePipelinesCredential(tenantID, clientID, connectionID, nil)
require.NoError(t, err)
testGetTokenSuccess(t, cred, "https://vault.azure.net/.default")
})
}

Просмотреть файл

@ -29,6 +29,8 @@ extends:
SubscriptionConfigurations:
- $(sub-config-azure-cloud-test-resources)
- $(sub-config-identity-test-resources)
EnvVars:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
RunLiveTests: true
ServiceDirectory: azidentity
UsePipelineProxy: false