src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

provider.go (5038B)


      1 // Package logincreds implements AWS credential provision for sessions created
      2 // via an `aws login` command.
      3 package logincreds
      4 
      5 import (
      6 	"context"
      7 	"encoding/json"
      8 	"errors"
      9 	"fmt"
     10 	"io"
     11 	"os"
     12 
     13 	"github.com/aws/aws-sdk-go-v2/aws"
     14 	"github.com/aws/aws-sdk-go-v2/internal/sdk"
     15 	"github.com/aws/aws-sdk-go-v2/service/signin"
     16 	"github.com/aws/aws-sdk-go-v2/service/signin/types"
     17 )
     18 
     19 // ProviderName identifies the login provider.
     20 const ProviderName = "LoginProvider"
     21 
     22 // TokenAPIClient provides the interface for the login session's token
     23 // retrieval operation.
     24 type TokenAPIClient interface {
     25 	CreateOAuth2Token(context.Context, *signin.CreateOAuth2TokenInput, ...func(*signin.Options)) (*signin.CreateOAuth2TokenOutput, error)
     26 }
     27 
     28 // Provider supplies credentials for an `aws login` session.
     29 type Provider struct {
     30 	options Options
     31 }
     32 
     33 var _ aws.CredentialsProvider = (*Provider)(nil)
     34 
     35 // Options configures the Provider.
     36 type Options struct {
     37 	Client TokenAPIClient
     38 
     39 	// APIOptions to pass to the underlying CreateOAuth2Token operation.
     40 	ClientOptions []func(*signin.Options)
     41 
     42 	// The path to the cached login token.
     43 	CachedTokenFilepath string
     44 
     45 	// Whether to restrict file permissions on newly-written cache files.
     46 	// When true, files are created with 0600 on Unix.
     47 	RestrictPermissions bool
     48 
     49 	// The chain of providers that was used to create this provider.
     50 	//
     51 	// These values are for reporting purposes and are not meant to be set up
     52 	// directly.
     53 	CredentialSources []aws.CredentialSource
     54 }
     55 
     56 // New returns a new login session credentials provider.
     57 func New(client TokenAPIClient, path string, opts ...func(*Options)) *Provider {
     58 	options := Options{
     59 		Client:              client,
     60 		CachedTokenFilepath: path,
     61 	}
     62 
     63 	for _, opt := range opts {
     64 		opt(&options)
     65 	}
     66 
     67 	return &Provider{options}
     68 }
     69 
     70 // Retrieve generates a new set of temporary credentials using an `aws login`
     71 // session.
     72 func (p *Provider) Retrieve(ctx context.Context) (aws.Credentials, error) {
     73 	token, err := p.loadToken()
     74 	if err != nil {
     75 		return aws.Credentials{}, fmt.Errorf("load login token: %w", err)
     76 	}
     77 	if err := token.Validate(); err != nil {
     78 		return aws.Credentials{}, fmt.Errorf("validate login token: %w", err)
     79 	}
     80 
     81 	// the token may have been refreshed elsewhere or the login session might
     82 	// have just been created
     83 	if sdk.NowTime().Before(token.AccessToken.ExpiresAt) {
     84 		return token.Credentials(), nil
     85 	}
     86 
     87 	opts := make([]func(*signin.Options), len(p.options.ClientOptions)+1)
     88 	opts[0] = addSignDPOP(token)
     89 	copy(opts[1:], p.options.ClientOptions)
     90 
     91 	out, err := p.options.Client.CreateOAuth2Token(ctx, &signin.CreateOAuth2TokenInput{
     92 		TokenInput: &types.CreateOAuth2TokenRequestBody{
     93 			ClientId:     aws.String(token.ClientID),
     94 			GrantType:    aws.String("refresh_token"),
     95 			RefreshToken: aws.String(token.RefreshToken),
     96 		},
     97 	}, opts...)
     98 	if err != nil {
     99 		var terr *types.AccessDeniedException
    100 		if errors.As(err, &terr) {
    101 			err = toAccessDeniedError(terr)
    102 		}
    103 		return aws.Credentials{}, fmt.Errorf("create oauth2 token: %w", err)
    104 	}
    105 
    106 	token.Update(out)
    107 	if err := p.saveToken(token); err != nil {
    108 		return aws.Credentials{}, fmt.Errorf("save token: %w", err)
    109 	}
    110 
    111 	return token.Credentials(), nil
    112 }
    113 
    114 // ProviderSources returns the credential chain that was used to construct this
    115 // provider.
    116 func (p *Provider) ProviderSources() []aws.CredentialSource {
    117 	if p.options.CredentialSources == nil {
    118 		return []aws.CredentialSource{aws.CredentialSourceLogin}
    119 	}
    120 	return p.options.CredentialSources
    121 }
    122 
    123 func (p *Provider) loadToken() (*loginToken, error) {
    124 	f, err := openFile(p.options.CachedTokenFilepath)
    125 	if err != nil && os.IsNotExist(err) {
    126 		return nil, fmt.Errorf("token file not found, please reauthenticate")
    127 	}
    128 	if err != nil {
    129 		return nil, err
    130 	}
    131 	defer f.Close()
    132 
    133 	j, err := io.ReadAll(f)
    134 	if err != nil {
    135 		return nil, err
    136 	}
    137 
    138 	var t *loginToken
    139 	if err := json.Unmarshal(j, &t); err != nil {
    140 		return nil, err
    141 	}
    142 
    143 	return t, nil
    144 }
    145 
    146 func (p *Provider) saveToken(token *loginToken) error {
    147 	j, err := json.Marshal(token)
    148 	if err != nil {
    149 		return err
    150 	}
    151 
    152 	mode := os.FileMode(0666) // matches that used by os.Create
    153 	if p.options.RestrictPermissions {
    154 		mode = 0600
    155 	}
    156 
    157 	// createFile DOES NOT re-create the file with new permissions if it
    158 	// already exists, so in that scenario any existing permissions are
    159 	// preserved
    160 	f, err := createFile(p.options.CachedTokenFilepath, mode)
    161 	if err != nil {
    162 		return err
    163 	}
    164 	defer f.Close()
    165 
    166 	if _, err := f.Write(j); err != nil {
    167 		return err
    168 	}
    169 
    170 	return nil
    171 }
    172 
    173 func toAccessDeniedError(err *types.AccessDeniedException) error {
    174 	switch err.Error_ {
    175 	case types.OAuth2ErrorCodeTokenExpired:
    176 		return fmt.Errorf("login session has expired, please reauthenticate")
    177 	case types.OAuth2ErrorCodeUserCredentialsChanged:
    178 		return fmt.Errorf("login session password has changed, please reauthenticate")
    179 	case types.OAuth2ErrorCodeInsufficientPermissions:
    180 		return fmt.Errorf("insufficient permissions, you may be missing permissions for the CreateOAuth2Token action")
    181 	default:
    182 		return err
    183 	}
    184 }