216 lines
6.0 KiB
Go
216 lines
6.0 KiB
Go
package authprovider
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
authutil "github.com/containerd/containerd/remotes/docker/auth"
|
|
remoteserrors "github.com/containerd/containerd/remotes/errors"
|
|
"github.com/docker/cli/cli/config"
|
|
"github.com/docker/cli/cli/config/configfile"
|
|
"github.com/moby/buildkit/session"
|
|
"github.com/moby/buildkit/session/auth"
|
|
"github.com/moby/buildkit/util/progress/progresswriter"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/nacl/sign"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func NewDockerAuthProvider(stderr io.Writer) session.Attachable {
|
|
return &authProvider{
|
|
config: config.LoadDefaultConfigFile(stderr),
|
|
seeds: &tokenSeeds{dir: config.Dir()},
|
|
loggerCache: map[string]struct{}{},
|
|
}
|
|
}
|
|
|
|
type authProvider struct {
|
|
config *configfile.ConfigFile
|
|
seeds *tokenSeeds
|
|
logger progresswriter.Logger
|
|
loggerCache map[string]struct{}
|
|
|
|
// The need for this mutex is not well understood.
|
|
// Without it, the docker cli on OS X hangs when
|
|
// reading credentials from docker-credential-osxkeychain.
|
|
// See issue https://github.com/docker/cli/issues/1862
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (ap *authProvider) SetLogger(l progresswriter.Logger) {
|
|
ap.mu.Lock()
|
|
ap.logger = l
|
|
ap.mu.Unlock()
|
|
}
|
|
|
|
func (ap *authProvider) Register(server *grpc.Server) {
|
|
auth.RegisterAuthServer(server, ap)
|
|
}
|
|
|
|
func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequest) (rr *auth.FetchTokenResponse, err error) {
|
|
creds, err := ap.credentials(req.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
to := authutil.TokenOptions{
|
|
Realm: req.Realm,
|
|
Service: req.Service,
|
|
Scopes: req.Scopes,
|
|
Username: creds.Username,
|
|
Secret: creds.Secret,
|
|
}
|
|
|
|
if creds.Secret != "" {
|
|
done := func(progresswriter.SubLogger) error {
|
|
return err
|
|
}
|
|
defer func() {
|
|
err = errors.Wrap(err, "failed to fetch oauth token")
|
|
}()
|
|
ap.mu.Lock()
|
|
name := fmt.Sprintf("[auth] %v token for %s", strings.Join(trimScopePrefix(req.Scopes), " "), req.Host)
|
|
if _, ok := ap.loggerCache[name]; !ok {
|
|
progresswriter.Wrap(name, ap.logger, done)
|
|
}
|
|
ap.mu.Unlock()
|
|
// credential information is provided, use oauth POST endpoint
|
|
resp, err := authutil.FetchTokenWithOAuth(ctx, http.DefaultClient, nil, "buildkit-client", to)
|
|
if err != nil {
|
|
var errStatus remoteserrors.ErrUnexpectedStatus
|
|
if errors.As(err, &errStatus) {
|
|
// Registries without support for POST may return 404 for POST /v2/token.
|
|
// As of September 2017, GCR is known to return 404.
|
|
// As of February 2018, JFrog Artifactory is known to return 401.
|
|
if (errStatus.StatusCode == 405 && to.Username != "") || errStatus.StatusCode == 404 || errStatus.StatusCode == 401 {
|
|
resp, err := authutil.FetchToken(ctx, http.DefaultClient, nil, to)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return toTokenResponse(resp.Token, resp.IssuedAt, resp.ExpiresIn), nil
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
return toTokenResponse(resp.AccessToken, resp.IssuedAt, resp.ExpiresIn), nil
|
|
}
|
|
// do request anonymously
|
|
resp, err := authutil.FetchToken(ctx, http.DefaultClient, nil, to)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to fetch anonymous token")
|
|
}
|
|
return toTokenResponse(resp.Token, resp.IssuedAt, resp.ExpiresIn), nil
|
|
}
|
|
|
|
func (ap *authProvider) credentials(host string) (*auth.CredentialsResponse, error) {
|
|
ap.mu.Lock()
|
|
defer ap.mu.Unlock()
|
|
if host == "registry-1.docker.io" {
|
|
host = "https://index.docker.io/v1/"
|
|
}
|
|
ac, err := ap.config.GetAuthConfig(host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res := &auth.CredentialsResponse{}
|
|
if ac.IdentityToken != "" {
|
|
res.Secret = ac.IdentityToken
|
|
} else {
|
|
res.Username = ac.Username
|
|
res.Secret = ac.Password
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (ap *authProvider) Credentials(ctx context.Context, req *auth.CredentialsRequest) (*auth.CredentialsResponse, error) {
|
|
resp, err := ap.credentials(req.Host)
|
|
if err != nil || resp.Secret != "" {
|
|
ap.mu.Lock()
|
|
defer ap.mu.Unlock()
|
|
_, ok := ap.loggerCache[req.Host]
|
|
ap.loggerCache[req.Host] = struct{}{}
|
|
if !ok {
|
|
return resp, progresswriter.Wrap(fmt.Sprintf("[auth] sharing credentials for %s", req.Host), ap.logger, func(progresswriter.SubLogger) error {
|
|
return err
|
|
})
|
|
}
|
|
}
|
|
return resp, err
|
|
}
|
|
|
|
func (ap *authProvider) GetTokenAuthority(ctx context.Context, req *auth.GetTokenAuthorityRequest) (*auth.GetTokenAuthorityResponse, error) {
|
|
key, err := ap.getAuthorityKey(req.Host, req.Salt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &auth.GetTokenAuthorityResponse{PublicKey: key[32:]}, nil
|
|
}
|
|
|
|
func (ap *authProvider) VerifyTokenAuthority(ctx context.Context, req *auth.VerifyTokenAuthorityRequest) (*auth.VerifyTokenAuthorityResponse, error) {
|
|
key, err := ap.getAuthorityKey(req.Host, req.Salt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
priv := new([64]byte)
|
|
copy((*priv)[:], key)
|
|
|
|
return &auth.VerifyTokenAuthorityResponse{Signed: sign.Sign(nil, req.Payload, priv)}, nil
|
|
}
|
|
|
|
func (ap *authProvider) getAuthorityKey(host string, salt []byte) (ed25519.PrivateKey, error) {
|
|
if v, err := strconv.ParseBool(os.Getenv("BUILDKIT_NO_CLIENT_TOKEN")); err == nil && v {
|
|
return nil, status.Errorf(codes.Unavailable, "client side tokens disabled")
|
|
}
|
|
|
|
creds, err := ap.credentials(host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seed, err := ap.seeds.getSeed(host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mac := hmac.New(sha256.New, salt)
|
|
if creds.Secret != "" {
|
|
mac.Write(seed)
|
|
}
|
|
|
|
sum := mac.Sum(nil)
|
|
|
|
return ed25519.NewKeyFromSeed(sum[:ed25519.SeedSize]), nil
|
|
}
|
|
|
|
func toTokenResponse(token string, issuedAt time.Time, expires int) *auth.FetchTokenResponse {
|
|
resp := &auth.FetchTokenResponse{
|
|
Token: token,
|
|
ExpiresIn: int64(expires),
|
|
}
|
|
if !issuedAt.IsZero() {
|
|
resp.IssuedAt = issuedAt.Unix()
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func trimScopePrefix(scopes []string) []string {
|
|
out := make([]string, len(scopes))
|
|
for i, s := range scopes {
|
|
out[i] = strings.TrimPrefix(s, "repository:")
|
|
}
|
|
return out
|
|
}
|