Path: blob/main/components/image-builder-mk3/pkg/auth/auth.go
2500 views
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package auth56import (7"bytes"8"context"9"crypto/sha256"10"encoding/base64"11"fmt"12"os"13"regexp"14"strings"15"sync"16"time"1718"github.com/aws/aws-sdk-go-v2/aws"19"github.com/aws/aws-sdk-go-v2/service/ecr"20"github.com/distribution/reference"21"github.com/docker/cli/cli/config/configfile"22"github.com/docker/docker/api/types/registry"23"github.com/opentracing/opentracing-go"24"golang.org/x/xerrors"2526"github.com/gitpod-io/gitpod/common-go/log"27"github.com/gitpod-io/gitpod/common-go/tracing"28"github.com/gitpod-io/gitpod/common-go/watch"29"github.com/gitpod-io/gitpod/image-builder/api"30)3132// RegistryAuthenticator can provide authentication for some registries33type RegistryAuthenticator interface {34// Authenticate attempts to provide authentication for Docker registry access35Authenticate(ctx context.Context, registry string) (auth *Authentication, err error)36}3738// NewDockerConfigFileAuth reads a docker config file to provide authentication39func NewDockerConfigFileAuth(fn string) (*DockerConfigFileAuth, error) {40res := &DockerConfigFileAuth{}41err := res.loadFromFile(fn)42if err != nil {43return nil, err44}4546err = watch.File(context.Background(), fn, func() {47res.loadFromFile(fn)48})49if err != nil {50log.WithError(err).WithField("path", fn).Error("error watching file")51return nil, err52}5354return res, nil55}5657// DockerConfigFileAuth uses a Docker config file to provide authentication58type DockerConfigFileAuth struct {59C *configfile.ConfigFile6061hash string62mu sync.RWMutex63}6465func (a *DockerConfigFileAuth) loadFromFile(fn string) (err error) {66defer func() {67if err != nil {68err = fmt.Errorf("error loading Docker config from %s: %w", fn, err)69log.WithError(err).WithField("path", fn).Error("failed loading from file")70}71}()7273cntnt, err := os.ReadFile(fn)74if err != nil {75return err76}77hash := sha256.New()78_, _ = hash.Write(cntnt)79newHash := fmt.Sprintf("%x", hash.Sum(nil))80if a.hash == newHash {81log.Infof("nothing has changed: %s", fn)82return nil83}8485log.WithField("path", fn).Info("reloading auth from Docker config")8687cfg := configfile.New(fn)88err = cfg.LoadFromReader(bytes.NewReader(cntnt))89if err != nil {90return err91}9293a.mu.Lock()94defer a.mu.Unlock()95a.C = cfg96a.hash = newHash9798log.Infof("file has changed: %s", fn)99return nil100}101102// Authenticate attempts to provide an encoded authentication string for Docker registry access103func (a *DockerConfigFileAuth) Authenticate(ctx context.Context, registry string) (auth *Authentication, err error) {104ac, err := a.C.GetAuthConfig(registry)105if err != nil {106log.WithError(err).WithField("registry", registry).Error("failed DockerConfigFileAuth Authenticate")107return nil, err108}109110return &Authentication{111Username: ac.Username,112Password: ac.Password,113Auth: ac.Auth,114Email: ac.Email,115ServerAddress: ac.ServerAddress,116IdentityToken: ac.IdentityToken,117RegistryToken: ac.RegistryToken,118}, nil119}120121// CompositeAuth returns the first non-empty authentication of any of its consitutents122type CompositeAuth []RegistryAuthenticator123124func (ca CompositeAuth) Authenticate(ctx context.Context, registry string) (auth *Authentication, err error) {125for _, ath := range ca {126res, err := ath.Authenticate(ctx, registry)127if err != nil {128log.WithError(err).WithField("registry", registry).Errorf("failed CompositeAuth Authenticate")129return nil, err130}131if !res.Empty() {132return res, nil133} else {134log.WithField("registry", registry).Warn("response was empty for CompositeAuth authenticate")135}136}137return &Authentication{}, nil138}139140func NewECRAuthenticator(ecrc *ecr.Client) *ECRAuthenticator {141return &ECRAuthenticator{142ecrc: ecrc,143}144}145146type ECRAuthenticator struct {147ecrc *ecr.Client148149ecrAuth string150ecrAuthLastRefreshTime time.Time151ecrAuthLock sync.Mutex152}153154const (155// ECR tokens are valid for 12h [1], and we want to ensure we refresh at least twice a day before full expiry.156//157// [1] https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html158ecrTokenRefreshTime = 4 * time.Hour159)160161func (ath *ECRAuthenticator) Authenticate(ctx context.Context, registry string) (auth *Authentication, err error) {162if !isECRRegistry(registry) {163return nil, nil164}165166defer func() {167if err != nil {168err = fmt.Errorf("error with ECR authenticate: %w", err)169log.WithError(err).WithField("registry", registry).Error("failed ECR authenticate")170}171}()172173ath.ecrAuthLock.Lock()174defer ath.ecrAuthLock.Unlock()175if time.Since(ath.ecrAuthLastRefreshTime) > ecrTokenRefreshTime {176tknout, err := ath.ecrc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})177if err != nil {178return nil, err179}180if len(tknout.AuthorizationData) == 0 {181err = fmt.Errorf("no ECR authorization data received")182return nil, err183}184185pwd, err := base64.StdEncoding.DecodeString(aws.ToString(tknout.AuthorizationData[0].AuthorizationToken))186if err != nil {187return nil, err188}189190ath.ecrAuth = string(pwd)191ath.ecrAuthLastRefreshTime = time.Now()192log.Info("refreshed ECR token")193} else {194log.Info("no ECR token refresh necessary")195}196197segs := strings.Split(ath.ecrAuth, ":")198if len(segs) != 2 {199err = fmt.Errorf("cannot understand ECR token. Expected 2 segments, got %d", len(segs))200return nil, err201}202return &Authentication{203Username: segs[0],204Password: segs[1],205Auth: base64.StdEncoding.EncodeToString([]byte(ath.ecrAuth)),206}, nil207}208209// Authentication represents docker usable authentication210type Authentication registry.AuthConfig211212func (a *Authentication) Empty() bool {213if a == nil {214return true215}216if a.Auth == "" && a.Password == "" {217return true218}219return false220}221222var ecrRegistryRegexp = regexp.MustCompile(`\d{12}.dkr.ecr.\w+-\w+-\w+.amazonaws.com`)223224const DummyECRRegistryDomain = "000000000000.dkr.ecr.dummy-host-zone.amazonaws.com"225226// isECRRegistry returns true if the registry domain is an ECR registry227func isECRRegistry(domain string) bool {228return ecrRegistryRegexp.MatchString(domain)229}230231// AllowedAuthFor describes for which repositories authentication may be provided for232type AllowedAuthFor struct {233All bool234Explicit []string235Additional map[string]string236}237238// AllowedAuthForAll means auth for all repositories is allowed239func AllowedAuthForAll() AllowedAuthFor { return AllowedAuthFor{true, nil, nil} }240241// AllowedAuthForNone means auth for no repositories is allowed242func AllowedAuthForNone() AllowedAuthFor { return AllowedAuthFor{false, nil, nil} }243244// IsAllowNone returns true if we are to allow authentication for no repos245func (a AllowedAuthFor) IsAllowNone() bool {246return !a.All && len(a.Explicit) == 0247}248249// IsAllowAll returns true if we are to allow authentication for all repos250func (a AllowedAuthFor) IsAllowAll() bool {251return a.All252}253254// Elevate adds a ref to the list of authenticated repositories255func (a AllowedAuthFor) Elevate(ref string) AllowedAuthFor {256pref, _ := reference.ParseNormalizedNamed(ref)257if pref == nil {258log.WithField("ref", ref).Debug("cannot elevate auth for invalid image ref")259return a260}261262return AllowedAuthFor{a.All, append(a.Explicit, reference.Domain(pref)), a.Additional}263}264265// ExplicitlyAll produces an AllowedAuthFor that allows authentication for all266// registries, yet carries the original Explicit list which affects GetAuthForImageBuild267func (a AllowedAuthFor) ExplicitlyAll() AllowedAuthFor {268return AllowedAuthFor{269All: true,270Explicit: a.Explicit,271}272}273274// Resolver resolves an auth request determining which authentication is actually allowed275type Resolver struct {276BaseImageRepository string277WorkspaceImageRepository string278}279280// ResolveRequestAuth computes the allowed authentication for a build based on its request281func (r Resolver) ResolveRequestAuth(ctx context.Context, auth *api.BuildRegistryAuth) (authFor AllowedAuthFor) {282span, _ := opentracing.StartSpanFromContext(ctx, "ResolveRequestAuth")283var err error284defer tracing.FinishSpan(span, &err)285286// by default we allow nothing287authFor = AllowedAuthForNone()288if auth == nil {289return290}291292switch ath := auth.Mode.(type) {293case *api.BuildRegistryAuth_Total:294if ath.Total.AllowAll {295authFor = AllowedAuthForAll()296} else {297authFor = AllowedAuthForNone()298}299case *api.BuildRegistryAuth_Selective:300var explicit []string301if ath.Selective.AllowBaserep {302ref, _ := reference.ParseNormalizedNamed(r.BaseImageRepository)303explicit = append(explicit, reference.Domain(ref))304}305if ath.Selective.AllowWorkspacerep {306ref, _ := reference.ParseNormalizedNamed(r.WorkspaceImageRepository)307explicit = append(explicit, reference.Domain(ref))308}309explicit = append(explicit, ath.Selective.AnyOf...)310authFor = AllowedAuthFor{false, explicit, nil}311default:312authFor = AllowedAuthForNone()313}314315authFor.Additional = auth.Additional316317return318}319320// GetAuthFor computes the base64 encoded auth format for a Docker image pull/push321func (a AllowedAuthFor) GetAuthFor(ctx context.Context, auth RegistryAuthenticator, refstr string) (res *Authentication, err error) {322if auth == nil {323return324}325326ref, err := reference.ParseNormalizedNamed(refstr)327if err != nil {328log.WithError(err).Errorf("failed parsing normalized name")329return nil, xerrors.Errorf("cannot parse image ref: %v", err)330}331reg := reference.Domain(ref)332333// If we haven't found authentication using the built-in way, we'll resort to additional auth334// the user sent us.335defer func() {336if err != nil || !res.Empty() {337return338}339340log.WithField("reg", reg).Debug("checking for additional auth")341res = a.additionalAuth(reg)342343if res != nil {344log.WithField("reg", reg).Debug("found additional auth")345}346}()347348var regAllowed bool349switch {350case a.IsAllowAll():351// free for all352regAllowed = true353case isECRRegistry(reg):354// We allow ECR registries by default to support private ECR registries OOTB.355// The AWS IAM permissions dictate what users actually have access to.356regAllowed = true357default:358for _, a := range a.Explicit {359if a == reg {360regAllowed = true361break362}363}364}365if !regAllowed {366log.WithField("reg", reg).WithField("ref", ref).WithField("a", a).Warn("registry not allowed - you may want to add this to the list of allowed registries in your installation config")367return nil, nil368}369370return auth.Authenticate(ctx, reg)371}372373func (a AllowedAuthFor) additionalAuth(domain string) *Authentication {374ath, ok := a.Additional[domain]375if !ok {376return nil377}378379res := &Authentication{380Auth: ath,381}382dec, err := base64.StdEncoding.DecodeString(ath)383if err == nil {384segs := strings.Split(string(dec), ":")385numSegs := len(segs)386387if numSegs > 1 {388res.Username = strings.Join(segs[:numSegs-1], ":")389res.Password = segs[numSegs-1]390}391} else {392log.Errorf("failed getting additional auth")393}394return res395}396397// ImageBuildAuth is the format image builds needs398type ImageBuildAuth map[string]registry.AuthConfig399400// GetImageBuildAuthFor produces authentication in the format an image builds needs401func (a AllowedAuthFor) GetImageBuildAuthFor(ctx context.Context, auth RegistryAuthenticator, additionalRegistries []string, blocklist []string) (res ImageBuildAuth) {402res = make(ImageBuildAuth)403for reg := range a.Additional {404var blocked bool405for _, blk := range blocklist {406if blk == reg {407blocked = true408break409}410}411if blocked {412continue413}414ath := a.additionalAuth(reg)415res[reg] = registry.AuthConfig(*ath)416}417for _, reg := range additionalRegistries {418ath, err := auth.Authenticate(ctx, reg)419if err != nil {420log.WithError(err).WithField("registry", reg).Warn("cannot get authentication for additional registry for image build")421continue422}423if ath.Empty() {424continue425}426res[reg] = registry.AuthConfig(*ath)427}428429return430}431432433