Skip to content

Commit

Permalink
Support Azure OIDC authentication
Browse files Browse the repository at this point in the history
- Add packages auth, azure to fetch access token using azidentity DefaultAzureCredential API and default ARM scope

- Provide the capability to override the scope of the access token for Azure DevOps.

- Provide the capability to pass proxy settings to the client options if specified.

- Provide the option to specify a fake token credential for unit tests.

- Add ProviderOptions in git AuthOptions to configure the provider options from consumers.

- Use the credentials API to fetch Azure DevOps access token if the provider is Azure from gogit client.

- Add new unit tests for new functionality in azure, git and gogit client.

Signed-off-by: Dipti Pai <[email protected]>
  • Loading branch information
dipti-pai committed Sep 12, 2024
1 parent a2a7a01 commit 8f8a78e
Show file tree
Hide file tree
Showing 16 changed files with 935 additions and 74 deletions.
118 changes: 118 additions & 0 deletions auth/azure/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azure

import (
"context"
"net/http"
"net/url"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)

const (
// 499b84ac-1321-427f-aa17-267ca6975798 is the UUID of Azure Devops
// resource. The scope defined below provides access to Azure DevOps
// Services REST API. The Microsoft Entra ID access token with this scope
// can be used to call Azure DevOps API by passing it in the headers as a
// Bearer Token : https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops#q-can-i-use-a-service-principal-or-managed-identity-with-azure-cli
AzureDevOpsRestApiScope = "499b84ac-1321-427f-aa17-267ca6975798/.default"
)

// Client is an authentication provider for Azure.
type Client struct {
credential azcore.TokenCredential
scopes []string
proxyURL *url.URL
}

// OptFunc enables specifying options for the provider.
type OptFunc func(*Client)

// New returns a new authentication provider for Azure. It configures
// credentials using a default credential chain with options.
// https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#NewDefaultAzureCredential
// The default scope is to ARM endpoint in Azure Cloud. The scope is overridden
// using OptFunc.
func New(opts ...OptFunc) (*Client, error) {
p := &Client{}
for _, opt := range opts {
opt(p)
}

clientOpts := &azidentity.DefaultAzureCredentialOptions{}

if p.proxyURL != nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = http.ProxyURL(p.proxyURL)
clientOpts.ClientOptions.Transport = &http.Client{Transport: transport}
}

if p.credential == nil {
cred, err := azidentity.NewDefaultAzureCredential(clientOpts)
if err != nil {
return nil, err
}
p.credential = cred
}

if len(p.scopes) == 0 {
p.scopes = []string{cloud.AzurePublic.Services[cloud.ResourceManager].Endpoint + "/" + ".default"}
}

return p, nil
}

// WithCredential configures the credential to use to fetch the resource manager
// token.
func WithCredential(cred azcore.TokenCredential) OptFunc {
return func(p *Client) {
p.credential = cred
}
}

// WithScope() configures the scope for GetToken requests
func WithScope(scopes []string) OptFunc {
return func(p *Client) {
p.scopes = append(p.scopes, scopes...)
}
}

// WithAzureDevOpsScope() configures the scope to access Azure DevOps Rest API
// needed to access Azure DevOps Repositories
func WithAzureDevOpsScope() OptFunc {
return func(p *Client) {
p.scopes = append(p.scopes, AzureDevOpsRestApiScope)
}
}

// WithProxyURL sets the proxy URL to use with NewDefaultAzureCredential.
func WithProxyURL(proxyURL *url.URL) OptFunc {
return func(p *Client) {
p.proxyURL = proxyURL
}
}

// GetToken gets an OAuth token using azcore TokenCredential
func (p *Client) GetToken(ctx context.Context) (azcore.AccessToken, error) {
return p.credential.GetToken(ctx, policy.TokenRequestOptions{
Scopes: p.scopes,
})
}
116 changes: 116 additions & 0 deletions auth/azure/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azure

import (
"context"
"errors"
"fmt"
"net/url"
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
. "github.com/onsi/gomega"
)

func TestGetProviderToken(t *testing.T) {
proxy, _ := url.Parse("http://localhost:8080")
tests := []struct {
name string
tokenCred azcore.TokenCredential
opts []OptFunc
wantToken string
wantScope string
wantErr error
}{
{
name: "azure devops scope",
tokenCred: &FakeTokenCredential{
Token: "foo",
},
opts: []OptFunc{WithAzureDevOpsScope()},
wantScope: AzureDevOpsRestApiScope,
wantToken: "foo",
},
{
name: "custom scope",
tokenCred: &FakeTokenCredential{
Token: "foo",
},
opts: []OptFunc{WithScope([]string{"custom scope"})},
wantScope: "custom scope",
wantToken: "foo",
},
{
name: "custom scope and azure devops scope",
tokenCred: &FakeTokenCredential{
Token: "foo",
},
opts: []OptFunc{WithScope([]string{"custom scope"}), WithAzureDevOpsScope()},
wantScope: fmt.Sprintf("custom scope,%s", AzureDevOpsRestApiScope),
wantToken: "foo",
},
{
name: "no scope specified",
tokenCred: &FakeTokenCredential{
Token: "foo",
},
wantScope: cloud.AzurePublic.Services[cloud.ResourceManager].Endpoint + "/" + ".default",
wantToken: "foo",
},
{
name: "with proxy url",
tokenCred: &FakeTokenCredential{
Token: "foo",
},
opts: []OptFunc{WithProxyURL(proxy)},
wantToken: "foo",
},
{
name: "error",
tokenCred: &FakeTokenCredential{
Err: errors.New("oh no!"),
},
wantErr: errors.New("oh no!"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
tt.opts = append(tt.opts, WithCredential(tt.tokenCred))
client, err := New(tt.opts...)
g.Expect(err).ToNot(HaveOccurred())
str := ""
ctx := context.WithValue(context.TODO(), "scope", &str)
token, err := client.GetToken(ctx)

if tt.wantErr != nil {
g.Expect(err).To(HaveOccurred())
g.Expect(err).To(Equal(tt.wantErr))
} else {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(token.Token).To(Equal(tt.wantToken))
if tt.wantScope != "" {
scope := ctx.Value("scope").(*string)
g.Expect(*scope).To(Equal(tt.wantScope))
}
}
})
}
}
51 changes: 51 additions & 0 deletions auth/azure/fake_credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azure

import (
"context"
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)

// FakeTokenCredential is a fake Azure credential provider.
type FakeTokenCredential struct {
Token string

ExpiresOn time.Time
Err error
}

var _ azcore.TokenCredential = &FakeTokenCredential{}

func (tc *FakeTokenCredential) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) {
if tc.Err != nil {
return azcore.AccessToken{}, tc.Err
}

// Embed the scope inside the context to verify that the desired scope was
// specified while fetching the token.
val, ok := ctx.Value("scope").(*string)
if ok {
*val = strings.Join(options.Scopes, ",")
}

return azcore.AccessToken{Token: tc.Token, ExpiresOn: tc.ExpiresOn}, nil
}
27 changes: 27 additions & 0 deletions auth/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module github.com/fluxcd/pkg/auth

go 1.22.4

require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
github.com/onsi/gomega v1.33.1
)

require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.24.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
56 changes: 56 additions & 0 deletions auth/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g=
github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading

0 comments on commit 8f8a78e

Please sign in to comment.