github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/auth/shared/oauth_scopes.go (about)

     1  package shared
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/http"
     7  	"strings"
     8  
     9  	"github.com/ungtb10d/cli/v2/api"
    10  	"github.com/ungtb10d/cli/v2/internal/ghinstance"
    11  )
    12  
    13  type MissingScopesError struct {
    14  	MissingScopes []string
    15  }
    16  
    17  func (e MissingScopesError) Error() string {
    18  	var missing []string
    19  	for _, s := range e.MissingScopes {
    20  		missing = append(missing, fmt.Sprintf("'%s'", s))
    21  	}
    22  	scopes := strings.Join(missing, ", ")
    23  
    24  	if len(e.MissingScopes) == 1 {
    25  		return "missing required scope " + scopes
    26  	}
    27  	return "missing required scopes " + scopes
    28  }
    29  
    30  type httpClient interface {
    31  	Do(*http.Request) (*http.Response, error)
    32  }
    33  
    34  func GetScopes(httpClient httpClient, hostname, authToken string) (string, error) {
    35  	apiEndpoint := ghinstance.RESTPrefix(hostname)
    36  
    37  	req, err := http.NewRequest("GET", apiEndpoint, nil)
    38  	if err != nil {
    39  		return "", err
    40  	}
    41  
    42  	req.Header.Set("Authorization", "token "+authToken)
    43  
    44  	res, err := httpClient.Do(req)
    45  	if err != nil {
    46  		return "", err
    47  	}
    48  
    49  	defer func() {
    50  		// Ensure the response body is fully read and closed
    51  		// before we reconnect, so that we reuse the same TCPconnection.
    52  		_, _ = io.Copy(io.Discard, res.Body)
    53  		res.Body.Close()
    54  	}()
    55  
    56  	if res.StatusCode != 200 {
    57  		return "", api.HandleHTTPError(res)
    58  	}
    59  
    60  	return res.Header.Get("X-Oauth-Scopes"), nil
    61  }
    62  
    63  func HasMinimumScopes(httpClient httpClient, hostname, authToken string) error {
    64  	scopesHeader, err := GetScopes(httpClient, hostname, authToken)
    65  	if err != nil {
    66  		return err
    67  	}
    68  
    69  	if scopesHeader == "" {
    70  		// if the token reports no scopes, assume that it's an integration token and give up on
    71  		// detecting its capabilities
    72  		return nil
    73  	}
    74  
    75  	search := map[string]bool{
    76  		"repo":      false,
    77  		"read:org":  false,
    78  		"admin:org": false,
    79  	}
    80  	for _, s := range strings.Split(scopesHeader, ",") {
    81  		search[strings.TrimSpace(s)] = true
    82  	}
    83  
    84  	var missingScopes []string
    85  	if !search["repo"] {
    86  		missingScopes = append(missingScopes, "repo")
    87  	}
    88  
    89  	if !search["read:org"] && !search["write:org"] && !search["admin:org"] {
    90  		missingScopes = append(missingScopes, "read:org")
    91  	}
    92  
    93  	if len(missingScopes) > 0 {
    94  		return &MissingScopesError{MissingScopes: missingScopes}
    95  	}
    96  	return nil
    97  }