Skip to content

Commit

Permalink
Implement new OIDC Login method
Browse files Browse the repository at this point in the history
Signed-off-by: Somtochi Onyekwere <[email protected]>
  • Loading branch information
somtochiama committed May 18, 2023
1 parent 4df95a6 commit 5256e34
Show file tree
Hide file tree
Showing 14 changed files with 295 additions and 84 deletions.
19 changes: 14 additions & 5 deletions oci/auth/aws/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (c *Client) WithConfig(cfg *aws.Config) {
// be the case if it's running in EKS, and may need additional setup
// otherwise (visit https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/
// as a starting point).
func (c *Client) getLoginAuth(ctx context.Context, awsEcrRegion string) (authn.AuthConfig, error) {
func (c *Client) getLoginAuth(ctx context.Context) (authn.AuthConfig, error) {
// No caching of tokens is attempted; the quota for getting an
// auth token is high enough that getting a token every time you
// scan an image is viable for O(500) images per region. See
Expand All @@ -91,7 +91,7 @@ func (c *Client) getLoginAuth(ctx context.Context, awsEcrRegion string) (authn.A
cfg = c.config.Copy()
} else {
var err error
cfg, err = config.LoadDefaultConfig(ctx, config.WithRegion(awsEcrRegion))
cfg, err = config.LoadDefaultConfig(ctx)
if err != nil {
c.mu.Unlock()
return authConfig, fmt.Errorf("failed to load default configuration: %w", err)
Expand Down Expand Up @@ -138,12 +138,11 @@ func (c *Client) getLoginAuth(ctx context.Context, awsEcrRegion string) (authn.A
func (c *Client) Login(ctx context.Context, autoLogin bool, image string) (authn.Authenticator, error) {
if autoLogin {
ctrl.LoggerFrom(ctx).Info("logging in to AWS ECR for " + image)
_, awsEcrRegion, ok := ParseRegistry(image)
_, _, ok := ParseRegistry(image)
if !ok {
return nil, errors.New("failed to parse AWS ECR image, invalid ECR image")
}

authConfig, err := c.getLoginAuth(ctx, awsEcrRegion)
authConfig, err := c.getLoginAuth(ctx)
if err != nil {
return nil, err
}
Expand All @@ -153,3 +152,13 @@ func (c *Client) Login(ctx context.Context, autoLogin bool, image string) (authn
}
return nil, fmt.Errorf("ECR authentication failed: %w", oci.ErrUnconfiguredProvider)
}

func (c *Client) OIDCLogin(ctx context.Context) (authn.Authenticator, error) {
authConfig, err := c.getLoginAuth(ctx)
if err != nil {
return nil, err
}

auth := authn.FromConfig(authConfig)
return auth, nil
}
11 changes: 10 additions & 1 deletion oci/auth/aws/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,11 @@ func TestGetLoginAuth(t *testing.T) {
cfg.EndpointResolverWithOptions = aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{URL: srv.URL}, nil
})
cfg.Region = "us-east-1"
cfg.Credentials = credentials.NewStaticCredentialsProvider("x", "y", "z")
ec.WithConfig(cfg)

a, err := ec.getLoginAuth(context.TODO(), "us-east-1")
a, err := ec.getLoginAuth(context.TODO())
g.Expect(err != nil).To(Equal(tt.wantErr))
if tt.statusCode == http.StatusOK {
g.Expect(a).To(Equal(tt.wantAuthConfig))
Expand All @@ -174,6 +175,7 @@ func TestLogin(t *testing.T) {
image string
statusCode int
wantErr bool
testOIDC bool
}{
{
name: "no auto login",
Expand All @@ -187,13 +189,15 @@ func TestLogin(t *testing.T) {
autoLogin: true,
image: testValidECRImage,
statusCode: http.StatusOK,
testOIDC: true,
},
{
name: "login failure",
autoLogin: true,
image: testValidECRImage,
statusCode: http.StatusInternalServerError,
wantErr: true,
testOIDC: true,
},
{
name: "non ECR image",
Expand Down Expand Up @@ -228,6 +232,11 @@ func TestLogin(t *testing.T) {

_, err := ecrClient.Login(context.TODO(), tt.autoLogin, tt.image)
g.Expect(err != nil).To(Equal(tt.wantErr))

if tt.testOIDC {
_, err = ecrClient.OIDCLogin(context.TODO())
g.Expect(err != nil).To(Equal(tt.wantErr))
}
})
}
}
17 changes: 14 additions & 3 deletions oci/auth/azure/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (c *Client) WithScheme(scheme string) *Client {

// getLoginAuth returns authentication for ACR. The details needed for authentication
// are gotten from environment variable so there is not need to mount a host path.
func (c *Client) getLoginAuth(ctx context.Context, ref name.Reference) (authn.AuthConfig, error) {
func (c *Client) getLoginAuth(ctx context.Context, endpoint string) (authn.AuthConfig, error) {
var authConfig authn.AuthConfig

// Use default credentials if no token credential is provided.
Expand All @@ -83,7 +83,6 @@ func (c *Client) getLoginAuth(ctx context.Context, ref name.Reference) (authn.Au
}

// Obtain ACR access token using exchanger.
endpoint := fmt.Sprintf("%s://%s", c.scheme, ref.Context().RegistryStr())
ex := newExchanger(endpoint)
accessToken, err := ex.ExchangeACRAccessToken(string(armToken.Token))
if err != nil {
Expand Down Expand Up @@ -114,7 +113,7 @@ func ValidHost(host string) bool {
func (c *Client) Login(ctx context.Context, autoLogin bool, image string, ref name.Reference) (authn.Authenticator, error) {
if autoLogin {
ctrl.LoggerFrom(ctx).Info("logging in to Azure ACR for " + image)
authConfig, err := c.getLoginAuth(ctx, ref)
authConfig, err := c.getLoginAuth(ctx, image)
if err != nil {
ctrl.LoggerFrom(ctx).Info("error logging into ACR " + err.Error())
return nil, err
Expand All @@ -125,3 +124,15 @@ func (c *Client) Login(ctx context.Context, autoLogin bool, image string, ref na
}
return nil, fmt.Errorf("ACR authentication failed: %w", oci.ErrUnconfiguredProvider)
}

// OIDCLogin attempts to get the authentication material for ACR from the registry url passed into the function.
func (c *Client) OIDCLogin(ctx context.Context, registryUrl string) (authn.Authenticator, error) {
authConfig, err := c.getLoginAuth(ctx, registryUrl)
if err != nil {
ctrl.LoggerFrom(ctx).Info("error logging into ACR " + err.Error())
return nil, err
}

auth := authn.FromConfig(authConfig)
return auth, nil
}
19 changes: 10 additions & 9 deletions oci/auth/azure/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,12 @@ func TestGetAzureLoginAuth(t *testing.T) {
srv.Close()
})

// Construct an image repo name against the test server.
u, err := url.Parse(srv.URL)
g.Expect(err).ToNot(HaveOccurred())
image := path.Join(u.Host, "foo/bar:v1")
ref, err := name.ParseReference(image)
g.Expect(err).ToNot(HaveOccurred())

// Configure new client with test token credential.
c := NewClient().
WithTokenCredential(tt.tokenCredential).
WithScheme("http")

auth, err := c.getLoginAuth(context.TODO(), ref)
auth, err := c.getLoginAuth(context.TODO(), srv.URL)
g.Expect(err != nil).To(Equal(tt.wantErr))
if tt.statusCode == http.StatusOK {
g.Expect(auth).To(Equal(tt.wantAuthConfig))
Expand Down Expand Up @@ -126,6 +119,7 @@ func TestLogin(t *testing.T) {
autoLogin bool
statusCode int
wantErr bool
testOIDC bool
}{
{
name: "no auto login",
Expand All @@ -137,12 +131,14 @@ func TestLogin(t *testing.T) {
name: "with auto login",
autoLogin: true,
statusCode: http.StatusOK,
testOIDC: true,
},
{
name: "login failure",
autoLogin: true,
statusCode: http.StatusInternalServerError,
wantErr: true,
testOIDC: true,
},
}

Expand Down Expand Up @@ -171,8 +167,13 @@ func TestLogin(t *testing.T) {
WithTokenCredential(&FakeTokenCredential{Token: "foo"}).
WithScheme("http")

_, err = ac.Login(context.TODO(), tt.autoLogin, image, ref)
_, err = ac.Login(context.TODO(), tt.autoLogin, srv.URL, ref)
g.Expect(err != nil).To(Equal(tt.wantErr))

if tt.testOIDC {
_, err = ac.OIDCLogin(context.TODO(), srv.URL)
g.Expect(err != nil).To(Equal(tt.wantErr))
}
})
}
}
9 changes: 8 additions & 1 deletion oci/auth/azure/exchanger.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ package azure
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
Expand Down Expand Up @@ -95,6 +96,7 @@ func (e *exchanger) ExchangeACRAccessToken(armToken string) (string, error) {
if err != nil {
return "", fmt.Errorf("failed to send token exchange request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
// Parse the error response.
Expand All @@ -112,7 +114,12 @@ func (e *exchanger) ExchangeACRAccessToken(armToken string) (string, error) {
var tokenResp tokenResponse
decoder := json.NewDecoder(resp.Body)
if err = decoder.Decode(&tokenResp); err != nil {
return "", fmt.Errorf("failed to decode the response: %w", err)
b, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return "", fmt.Errorf("failed to read the body of the response: %s", readErr)
}

return "", fmt.Errorf("failed to decode the response: %w, response body: %s", err, string(b))
}
return tokenResp.RefreshToken, nil
}
12 changes: 12 additions & 0 deletions oci/auth/gcp/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,15 @@ func (c *Client) Login(ctx context.Context, autoLogin bool, image string, ref na
}
return nil, fmt.Errorf("GCR authentication failed: %w", oci.ErrUnconfiguredProvider)
}

// OIDCLogin attempts to get the authentication material for GCR from the token url set in the client.
func (c *Client) OIDCLogin(ctx context.Context) (authn.Authenticator, error) {
authConfig, err := c.getLoginAuth(ctx)
if err != nil {
ctrl.LoggerFrom(ctx).Info("error logging into GCP " + err.Error())
return nil, err
}

auth := authn.FromConfig(authConfig)
return auth, nil
}
8 changes: 8 additions & 0 deletions oci/auth/gcp/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func TestLogin(t *testing.T) {
image string
statusCode int
wantErr bool
testOIDC bool
}{
{
name: "no auto login",
Expand All @@ -125,13 +126,15 @@ func TestLogin(t *testing.T) {
autoLogin: true,
image: testValidGCRImage,
statusCode: http.StatusOK,
testOIDC: true,
},
{
name: "login failure",
autoLogin: true,
image: testValidGCRImage,
statusCode: http.StatusInternalServerError,
wantErr: true,
testOIDC: true,
},
{
name: "non GCR image",
Expand Down Expand Up @@ -161,6 +164,11 @@ func TestLogin(t *testing.T) {

_, err = gc.Login(context.TODO(), tt.autoLogin, tt.image, ref)
g.Expect(err != nil).To(Equal(tt.wantErr))

if tt.testOIDC {
_, err = gc.OIDCLogin(context.TODO())
g.Expect(err != nil).To(Equal(tt.wantErr))
}
})
}
}
76 changes: 50 additions & 26 deletions oci/auth/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,45 @@ package login

import (
"context"
"strings"

"fmt"
util "github.com/fluxcd/pkg/oci/auth/utils"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"net/url"
ctrl "sigs.k8s.io/controller-runtime"

"github.com/fluxcd/pkg/oci"
"github.com/fluxcd/pkg/oci/auth/aws"
"github.com/fluxcd/pkg/oci/auth/azure"
"github.com/fluxcd/pkg/oci/auth/gcp"
)

// ImageRegistryProvider analyzes the provided registry and returns the identified
// container image registry provider.
func ImageRegistryProvider(url string, ref name.Reference) oci.Provider {
// If the url is a repository root address, use it to analyze. Else, derive
// the registry from the name reference.
// NOTE: This is because name.Reference of a repository root assumes that
// the reference is an image name and defaults to using index.docker.io as
// the registry host.
addr := strings.TrimSuffix(url, "/")
if strings.ContainsRune(addr, '/') {
addr = ref.Context().RegistryStr()
}
// GetCloudProvider analyzes the provided registry host and verifies that the right provider option is set
// It returns the identified container image registry provider or an error if the option isn't set.
func GetCloudProvider(registryHost string, options ProviderOptions) (oci.Provider, error) {
if _, _, ok := aws.ParseRegistry(registryHost); ok {
if !options.AwsAutoLogin {
return oci.ProviderGeneric, fmt.Errorf("ECR authentication failed: %w", oci.ErrUnconfiguredProvider)
}

_, _, ok := aws.ParseRegistry(addr)
if ok {
return oci.ProviderAWS
return oci.ProviderAWS, nil
}
if gcp.ValidHost(addr) {
return oci.ProviderGCP

if gcp.ValidHost(registryHost) {
if !options.GcpAutoLogin {
return oci.ProviderGeneric, fmt.Errorf("GCR authentication failed: %w", oci.ErrUnconfiguredProvider)
}
return oci.ProviderGCP, nil
}
if azure.ValidHost(addr) {
return oci.ProviderAzure

if azure.ValidHost(registryHost) {
if !options.AzureAutoLogin {
return oci.ProviderGeneric, fmt.Errorf("ACR authentication failed: %w", oci.ErrUnconfiguredProvider)
}
return oci.ProviderAzure, nil
}
return oci.ProviderGeneric

return oci.ProviderGeneric, nil
}

// ProviderOptions contains options for registry provider login.
Expand Down Expand Up @@ -106,13 +110,33 @@ func (m *Manager) WithACRClient(c *azure.Client) *Manager {
// Login performs authentication against a registry and returns the
// authentication material. For generic registry provider, it is no-op.
func (m *Manager) Login(ctx context.Context, url string, ref name.Reference, opts ProviderOptions) (authn.Authenticator, error) {
switch ImageRegistryProvider(url, ref) {
registryUrl := util.GetRegistryUrl(url, "https", ref)
return m.OIDCLogin(ctx, registryUrl, opts)
}

// OIDCLogin performs authentication against a registry and returns the authentication material.
// It accepts a context and the url of the registry. For generic registry provider, it is no-op.
func (m *Manager) OIDCLogin(ctx context.Context, registryUrl string, opts ProviderOptions) (authn.Authenticator, error) {
u, err := url.Parse(registryUrl)
if err != nil {
return nil, fmt.Errorf("unable to parse registry url: %w", err)
}

provider, err := GetCloudProvider(u.Host, opts)
if err != nil {
return nil, fmt.Errorf("unable to set up provider: %w", err)
}

switch provider {
case oci.ProviderAWS:
return m.ecr.Login(ctx, opts.AwsAutoLogin, url)
ctrl.LoggerFrom(ctx).Info("logging in to AWS ECR for " + u.Host)
return m.ecr.OIDCLogin(ctx)
case oci.ProviderGCP:
return m.gcr.Login(ctx, opts.GcpAutoLogin, url, ref)
ctrl.LoggerFrom(ctx).Info("logging in to GCP GCR for " + u.Host)
return m.gcr.OIDCLogin(ctx)
case oci.ProviderAzure:
return m.acr.Login(ctx, opts.AzureAutoLogin, url, ref)
ctrl.LoggerFrom(ctx).Info("logging in to Azure ACR for " + u.Host)
return m.acr.OIDCLogin(ctx, registryUrl)
}
return nil, nil
}
Loading

0 comments on commit 5256e34

Please sign in to comment.