github.com/htcondor/osdf-client/v6@v6.13.0-rc1.0.20231009141709-766e7b4d1dc8/acquire_token.go (about)

     1  package stashcp
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/url"
     7  	"path"
     8  	"strings"
     9  	"time"
    10  
    11  	config "github.com/htcondor/osdf-client/v6/config"
    12  	namespaces "github.com/htcondor/osdf-client/v6/namespaces"
    13  	log "github.com/sirupsen/logrus"
    14  	jwt "github.com/golang-jwt/jwt"
    15  	oauth2 "github.com/htcondor/osdf-client/v6/oauth2"
    16  	oauth2_upstream "golang.org/x/oauth2"
    17  )
    18  
    19  func TokenIsAcceptable(jwtSerialized string, osdfPath string, namespace namespaces.Namespace, isWrite bool) bool {
    20  	parser := jwt.Parser{SkipClaimsValidation: true}
    21  	token, _, err := parser.ParseUnverified(jwtSerialized, &jwt.MapClaims{})
    22  	if err != nil {
    23  		log.Warningln("Failed to parse token:", err)
    24  		return false
    25  	}
    26  
    27  	// For now, we'll accept any WLCG token
    28  	wlcg_ver := (*token.Claims.(*jwt.MapClaims))["wlcg.ver"]
    29  	if wlcg_ver == nil {
    30  		return false
    31  	}
    32  
    33  	osdfPathCleaned := path.Clean(osdfPath)
    34  	if !strings.HasPrefix(osdfPathCleaned, namespace.Path) {
    35  		return false
    36  	}
    37  
    38  	// For some issuers, the token base path is distinct from the OSDF base path.
    39  	// Example:
    40  	// - Issuer base path: `/chtc`
    41  	// - Namespace path: `/chtc/PROTECTED`
    42  	// In this case, we want to strip out the issuer base path, not the
    43  	// namespace one, in order to see if the token has the right privs.
    44  
    45  	targetResource := path.Clean("/" + osdfPathCleaned[len(namespace.Path):])
    46  	if namespace.CredentialGen != nil && namespace.CredentialGen.BasePath != nil && len(*namespace.CredentialGen.BasePath) > 0 {
    47  		targetResource = path.Clean("/" + osdfPathCleaned[len(*namespace.CredentialGen.BasePath):])
    48  	}
    49  
    50  	scopes_iface := (*token.Claims.(*jwt.MapClaims))["scope"]
    51  	if scopes, ok := scopes_iface.(string); ok {
    52  		acceptableScope := false
    53  		for _, scope := range strings.Split(scopes, " ") {
    54  			scope_info := strings.Split(scope, ":")
    55  			scopeOK := false
    56  			if isWrite && (scope_info[0] == "storage.modify" || scope_info[0] == "storage.create") {
    57  				scopeOK = true
    58  			} else if scope_info[0] == "storage.read" {
    59  				scopeOK = true
    60  			}
    61  			if !scopeOK {
    62  				continue
    63  			}
    64  
    65  			if len(scope_info) == 1 {
    66  				acceptableScope = true
    67  				break
    68  			}
    69  			if strings.HasPrefix(targetResource, scope_info[1]) {
    70  				acceptableScope = true
    71  				break
    72  			}
    73  		}
    74  		if acceptableScope {
    75  			return true
    76  		}
    77  	}
    78  	return false
    79  }
    80  
    81  func TokenIsExpired(jwtSerialized string) bool {
    82  	parser := jwt.Parser{SkipClaimsValidation: true}
    83  	token, _, err := parser.ParseUnverified(jwtSerialized, &jwt.StandardClaims{})
    84  	if err != nil {
    85  		log.Warningln("Failed to parse token:", err)
    86  		return true
    87  	}
    88  
    89  	if claims, ok := token.Claims.(*jwt.StandardClaims); ok {
    90  		return claims.Valid() != nil
    91  	}
    92  	return true
    93  }
    94  
    95  func RegisterClient(namespace namespaces.Namespace) (*config.PrefixEntry, error) {
    96  	issuer, err := oauth2.GetIssuerMetadata(*namespace.CredentialGen.Issuer)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	if issuer.RegistrationURL == "" {
   101  		return nil, fmt.Errorf("Issuer %s does not support dynamic client registration", *namespace.CredentialGen.Issuer)
   102  	}
   103  
   104  	drcp := oauth2.DCRPConfig{ClientRegistrationEndpointURL: issuer.RegistrationURL, Metadata: oauth2.Metadata{
   105  		RedirectURIs: []string{"https://localhost/osdf-client"},
   106  		TokenEndpointAuthMethod: "client_secret_basic",
   107  		GrantTypes: []string{"refresh_token", "urn:ietf:params:oauth:grant-type:device_code"},
   108  		ResponseTypes: []string{"code"},
   109  		ClientName: "OSDF Command Line Client",
   110  		Scopes: []string{"offline_access", "wlcg", "storage.read:/", "storage.modify:/", "storage.create:/"},
   111  	}}
   112  
   113  	resp, err := drcp.Register()
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	newEntry := config.PrefixEntry{
   118  		Prefix: namespace.Path,
   119  		ClientID: resp.ClientID,
   120  		ClientSecret: resp.ClientSecret,
   121  	}
   122  	return &newEntry, nil
   123  }
   124  
   125  // Given a URL and a piece of the namespace, attempt to acquire a valid
   126  // token for that URL.
   127  func AcquireToken(destination *url.URL, namespace namespaces.Namespace, isWrite bool) (string, error) {
   128  	log.Debugln("Acquiring a token from configuration and OAuth2")
   129  
   130  	if namespace.CredentialGen == nil || namespace.CredentialGen.Strategy == nil {
   131  		return "", fmt.Errorf("Credential generation scheme unknown for prefix %s", namespace.Path)
   132  	}
   133  	switch strategy := *namespace.CredentialGen.Strategy; strategy {
   134  	case "OAuth2":
   135  	case "Vault":
   136  		return "", fmt.Errorf("Vault credential generation strategy is not supported")
   137  	default:
   138  		return "", fmt.Errorf("Unknown credential generation strategy (%s) for prefix %s",
   139                                        strategy, namespace.Path)
   140  	}
   141  	issuer := *namespace.CredentialGen.Issuer
   142  	if len(issuer) == 0 {
   143  		return "", fmt.Errorf("Issuer for prefix %s is unknown", namespace.Path)
   144  	}
   145  
   146  	osdfConfig, err := config.GetConfigContents()
   147  	if err != nil {
   148  		return "", err
   149  	}
   150  
   151  	prefixIdx := -1
   152  	for idx, entry := range osdfConfig.OSDF.OauthClient {
   153  		if entry.Prefix == namespace.Path {
   154  			prefixIdx = idx
   155  			break
   156  		}
   157  	}
   158  	var prefixEntry *config.PrefixEntry
   159  	newEntry := false
   160  	if prefixIdx < 0 {
   161  		log.Infof("Prefix configuration for %s not in configuration file; will request new client", namespace.Path)
   162  		prefixEntry, err = RegisterClient(namespace)
   163  		if err != nil {
   164  			return "", err
   165  		}
   166  		osdfConfig.OSDF.OauthClient = append(osdfConfig.OSDF.OauthClient, *prefixEntry)
   167  		prefixEntry = &osdfConfig.OSDF.OauthClient[len(osdfConfig.OSDF.OauthClient) - 1]
   168  		newEntry = true
   169  	} else {
   170  		prefixEntry = &osdfConfig.OSDF.OauthClient[prefixIdx]
   171  		if len(prefixEntry.ClientID) == 0 || len(prefixEntry.ClientSecret) == 0 {
   172  			log.Infof("Prefix configuration for %s missing OAuth2 client information", namespace.Path)
   173  			prefixEntry, err = RegisterClient(namespace)
   174  			if err != nil {
   175  				return "", err
   176  			}
   177  			osdfConfig.OSDF.OauthClient[prefixIdx] = *prefixEntry
   178  			newEntry = true
   179  		}
   180  	}
   181  	if newEntry {
   182  		if err = config.SaveConfigContents(&osdfConfig); err != nil {
   183  			log.Warningln("Failed to save new token to configuration file:", err)
   184  		}
   185  	}
   186  
   187  	// For now, a fairly useless token-selection algorithm - take the first in the list.
   188  	// In the future, we should:
   189  	// - Check scopes
   190  	var acceptableToken *config.TokenEntry = nil
   191  	acceptableUnexpiredToken := ""
   192  	for idx, token := range prefixEntry.Tokens {
   193  		if !TokenIsAcceptable(token.AccessToken, destination.Path, namespace, isWrite) {
   194  			continue
   195  		}
   196  		if acceptableToken == nil {
   197  			acceptableToken = &prefixEntry.Tokens[idx]
   198  		} else if acceptableUnexpiredToken != "" {
   199  			// Both tokens are non-empty; let's use them
   200  			break
   201  		}
   202  		if !TokenIsExpired(token.AccessToken) {
   203  			acceptableUnexpiredToken = token.AccessToken
   204  		}
   205  	}
   206  	if len(acceptableUnexpiredToken) > 0 {
   207  		log.Debugln("Returning an unexpired token from cache")
   208  		return acceptableUnexpiredToken, nil
   209  	}
   210  
   211  	if acceptableToken != nil && len(acceptableToken.RefreshToken) > 0 {
   212  
   213  		// We have a reasonable token; let's try refreshing it.
   214  		upstreamToken := oauth2_upstream.Token{
   215  			AccessToken: acceptableToken.AccessToken,
   216  			RefreshToken: acceptableToken.RefreshToken,
   217  			Expiry: time.Unix(0, 0),
   218  		}
   219  		issuerInfo, err := oauth2.GetIssuerMetadata(issuer)
   220  		if err == nil {
   221  			upstreamConfig := oauth2_upstream.Config{
   222  				ClientID: prefixEntry.ClientID,
   223  				ClientSecret: prefixEntry.ClientSecret,
   224  				Endpoint: oauth2_upstream.Endpoint{
   225  					AuthURL: issuerInfo.AuthURL,
   226  					TokenURL: issuerInfo.TokenURL,
   227  				}}
   228  			ctx := context.Background()
   229  			source := upstreamConfig.TokenSource(ctx, &upstreamToken)
   230  			newToken, err := source.Token()
   231  			if err != nil {
   232  				log.Warningln("Failed to renew an expired token:", err)
   233  			} else {
   234  				acceptableToken.AccessToken = newToken.AccessToken
   235  				acceptableToken.Expiration = newToken.Expiry.Unix()
   236  				if len(newToken.RefreshToken) != 0 {
   237  					acceptableToken.RefreshToken = newToken.RefreshToken
   238  				}
   239  				if err = config.SaveConfigContents(&osdfConfig); err != nil {
   240  					log.Warningln("Failed to save new token to configuration file:", err)
   241  				}
   242  				return newToken.AccessToken, nil
   243  			}
   244  		}
   245  	}
   246  
   247  	token, err := oauth2.AcquireToken(issuer, prefixEntry, namespace.CredentialGen, destination.Path, isWrite)
   248  	if err != nil {
   249  		return "", err
   250  	}
   251  
   252  	Tokens := &prefixEntry.Tokens
   253  	*Tokens = append(*Tokens, *token)
   254  
   255  	if err = config.SaveConfigContents(&osdfConfig); err != nil {
   256  		log.Warningln("Failed to save new token to configuration file:", err)
   257  	}
   258  
   259  	return token.AccessToken, nil
   260  }
   261