-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- 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
Showing
16 changed files
with
934 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
Oops, something went wrong.