...

Source file src/github.com/cli/go-gh/v2/pkg/auth/auth.go

Documentation: github.com/cli/go-gh/v2/pkg/auth

     1  // Package auth is a set of functions for retrieving authentication tokens
     2  // and authenticated hosts.
     3  package auth
     4  
     5  import (
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"strings"
    10  
    11  	"github.com/cli/go-gh/v2/internal/set"
    12  	"github.com/cli/go-gh/v2/pkg/config"
    13  	"github.com/cli/safeexec"
    14  )
    15  
    16  const (
    17  	codespaces            = "CODESPACES"
    18  	defaultSource         = "default"
    19  	ghEnterpriseToken     = "GH_ENTERPRISE_TOKEN"
    20  	ghHost                = "GH_HOST"
    21  	ghToken               = "GH_TOKEN"
    22  	github                = "github.com"
    23  	githubEnterpriseToken = "GITHUB_ENTERPRISE_TOKEN"
    24  	githubToken           = "GITHUB_TOKEN"
    25  	hostsKey              = "hosts"
    26  	localhost             = "github.localhost"
    27  	oauthToken            = "oauth_token"
    28  	tenancyHost           = "ghe.com" // TenancyHost is the domain suffix of a tenancy GitHub instance.
    29  )
    30  
    31  // TokenForHost retrieves an authentication token and the source of that token for the specified
    32  // host. The source can be either an environment variable, configuration file, or the system
    33  // keyring. In the latter case, this shells out to "gh auth token" to obtain the token.
    34  //
    35  // Returns "", "default" if no applicable token is found.
    36  func TokenForHost(host string) (string, string) {
    37  	if token, source := TokenFromEnvOrConfig(host); token != "" {
    38  		return token, source
    39  	}
    40  
    41  	ghExe := os.Getenv("GH_PATH")
    42  	if ghExe == "" {
    43  		ghExe, _ = safeexec.LookPath("gh")
    44  	}
    45  
    46  	if ghExe != "" {
    47  		if token, source := tokenFromGh(ghExe, host); token != "" {
    48  			return token, source
    49  		}
    50  	}
    51  
    52  	return "", defaultSource
    53  }
    54  
    55  // TokenFromEnvOrConfig retrieves an authentication token from environment variables or the config
    56  // file as fallback, but does not support reading the token from system keyring. Most consumers
    57  // should use TokenForHost.
    58  func TokenFromEnvOrConfig(host string) (string, string) {
    59  	cfg, _ := config.Read(nil)
    60  	return tokenForHost(cfg, host)
    61  }
    62  
    63  func tokenForHost(cfg *config.Config, host string) (string, string) {
    64  	normalizedHost := NormalizeHostname(host)
    65  	// This code is currently the exact opposite of IsEnterprise. However, we have chosen
    66  	// to write it separately, directly in line, because it is much clearer in the exact
    67  	// scenarios that we expect to use GH_TOKEN and GITHUB_TOKEN.
    68  	if normalizedHost == github || IsTenancy(normalizedHost) || normalizedHost == localhost {
    69  		if token := os.Getenv(ghToken); token != "" {
    70  			return token, ghToken
    71  		}
    72  
    73  		if token := os.Getenv(githubToken); token != "" {
    74  			return token, githubToken
    75  		}
    76  	} else {
    77  		if token := os.Getenv(ghEnterpriseToken); token != "" {
    78  			return token, ghEnterpriseToken
    79  		}
    80  
    81  		if token := os.Getenv(githubEnterpriseToken); token != "" {
    82  			return token, githubEnterpriseToken
    83  		}
    84  	}
    85  
    86  	// If config is nil, something has failed much earlier and it's probably
    87  	// more correct to panic because we don't expect to support anything
    88  	// where the config isn't available, but that would be a breaking change,
    89  	// so it's worth thinking about carefully, if we wanted to rework this.
    90  	if cfg == nil {
    91  		return "", defaultSource
    92  	}
    93  
    94  	token, err := cfg.Get([]string{hostsKey, normalizedHost, oauthToken})
    95  	if err != nil {
    96  		return "", defaultSource
    97  	}
    98  
    99  	return token, oauthToken
   100  }
   101  
   102  func tokenFromGh(path string, host string) (string, string) {
   103  	cmd := exec.Command(path, "auth", "token", "--secure-storage", "--hostname", host)
   104  	result, err := cmd.Output()
   105  	if err != nil {
   106  		return "", "gh"
   107  	}
   108  	return strings.TrimSpace(string(result)), "gh"
   109  }
   110  
   111  // KnownHosts retrieves a list of hosts that have corresponding
   112  // authentication tokens, either from environment variables
   113  // or from the configuration file.
   114  // Returns an empty string slice if no hosts are found.
   115  func KnownHosts() []string {
   116  	cfg, _ := config.Read(nil)
   117  	return knownHosts(cfg)
   118  }
   119  
   120  func knownHosts(cfg *config.Config) []string {
   121  	hosts := set.NewStringSet()
   122  	if host := os.Getenv(ghHost); host != "" {
   123  		hosts.Add(host)
   124  	}
   125  	if token, _ := tokenForHost(cfg, github); token != "" {
   126  		hosts.Add(github)
   127  	}
   128  	if cfg != nil {
   129  		keys, err := cfg.Keys([]string{hostsKey})
   130  		if err == nil {
   131  			hosts.AddValues(keys)
   132  		}
   133  	}
   134  	return hosts.ToSlice()
   135  }
   136  
   137  // DefaultHost retrieves an authenticated host and the source of host.
   138  // The source can be either an environment variable or from the
   139  // configuration file.
   140  // Returns "github.com", "default" if no viable host is found.
   141  func DefaultHost() (string, string) {
   142  	cfg, _ := config.Read(nil)
   143  	return defaultHost(cfg)
   144  }
   145  
   146  func defaultHost(cfg *config.Config) (string, string) {
   147  	if host := os.Getenv(ghHost); host != "" {
   148  		return host, ghHost
   149  	}
   150  	if cfg != nil {
   151  		keys, err := cfg.Keys([]string{hostsKey})
   152  		if err == nil && len(keys) == 1 {
   153  			return keys[0], hostsKey
   154  		}
   155  	}
   156  	return github, defaultSource
   157  }
   158  
   159  // IsEnterprise determines if a provided host is a GitHub Enterprise Server instance,
   160  // rather than GitHub.com, a tenancy GitHub instance, or github.localhost.
   161  func IsEnterprise(host string) bool {
   162  	// Note that if you are making changes here, you should also consider making the equivalent
   163  	// in tokenForHost, which is the exact opposite of this function.
   164  	normalizedHost := NormalizeHostname(host)
   165  	return normalizedHost != github && normalizedHost != localhost && !IsTenancy(normalizedHost)
   166  }
   167  
   168  // IsTenancy determines if a provided host is a tenancy GitHub instance,
   169  // rather than GitHub.com or a GitHub Enterprise Server instance.
   170  func IsTenancy(host string) bool {
   171  	normalizedHost := NormalizeHostname(host)
   172  	return strings.HasSuffix(normalizedHost, "."+tenancyHost)
   173  }
   174  
   175  // NormalizeHostname ensures the host matches the values used throughout
   176  // the rest of the codebase with respect to hostnames. These are github,
   177  // localhost, and tenancyHost.
   178  func NormalizeHostname(host string) string {
   179  	hostname := strings.ToLower(host)
   180  	if strings.HasSuffix(hostname, "."+github) {
   181  		return github
   182  	}
   183  	if strings.HasSuffix(hostname, "."+localhost) {
   184  		return localhost
   185  	}
   186  	// This has been copied over from the cli/cli NormalizeHostname function
   187  	// to ensure compatible behaviour but we don't fully understand when or
   188  	// why it would be useful here. We can't see what harm will come of
   189  	// duplicating the logic.
   190  	if before, found := cutSuffix(hostname, "."+tenancyHost); found {
   191  		idx := strings.LastIndex(before, ".")
   192  		return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost)
   193  	}
   194  	return hostname
   195  }
   196  
   197  // Backport strings.CutSuffix from Go 1.20.
   198  func cutSuffix(s, suffix string) (string, bool) {
   199  	if !strings.HasSuffix(s, suffix) {
   200  		return s, false
   201  	}
   202  	return s[:len(s)-len(suffix)], true
   203  }
   204  

View as plain text