mirror of https://github.com/grafana/grafana
parent
1188f8df73
commit
fcdf282090
@ -1,56 +1,96 @@ |
||||
package imguploader |
||||
|
||||
import ( |
||||
"cloud.google.com/go/storage" |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"github.com/grafana/grafana/pkg/log" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
"golang.org/x/net/context" |
||||
"google.golang.org/api/option" |
||||
"golang.org/x/oauth2/google" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"os" |
||||
) |
||||
|
||||
type GCPUploader struct { |
||||
type GCSUploader struct { |
||||
keyFile string |
||||
bucket string |
||||
log log.Logger |
||||
} |
||||
|
||||
func NewGCPUploader(keyFile, bucket string) *GCPUploader { |
||||
return &GCPUploader{ |
||||
func NewGCSUploader(keyFile, bucket string) *GCSUploader { |
||||
return &GCSUploader{ |
||||
keyFile: keyFile, |
||||
bucket: bucket, |
||||
log: log.New("gcpuploader"), |
||||
log: log.New("gcsuploader"), |
||||
} |
||||
} |
||||
|
||||
func (u *GCPUploader) Upload(imageDiskPath string) (string, error) { |
||||
ctx := context.Background() |
||||
func (u *GCSUploader) Upload(imageDiskPath string) (string, error) { |
||||
key := util.GetRandomString(20) + ".png" |
||||
|
||||
client, err := storage.NewClient(ctx, option.WithServiceAccountFile(u.keyFile)) |
||||
log.Debug("Opening key file ", u.keyFile) |
||||
|
||||
ctx := context.Background() |
||||
data, err := ioutil.ReadFile(u.keyFile) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
key := util.GetRandomString(20) + ".png" |
||||
log.Debug("Uploading image to GCP bucket = %s key = %s", u.bucket, key) |
||||
log.Debug("Creating JWT conf") |
||||
|
||||
file, err := ioutil.ReadFile(imageDiskPath) |
||||
conf, err := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/devstorage.full_control") |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
wc := client.Bucket(u.bucket).Object(key).NewWriter(ctx) |
||||
wc.ContentType = "image/png" |
||||
wc.ACL = []storage.ACLRule{{Entity: storage.AllUsers, Role: storage.RoleReader}} |
||||
log.Debug("Creating HTTP client") |
||||
|
||||
if _, err := wc.Write(file); err != nil { |
||||
return "", err |
||||
} |
||||
client := conf.Client(ctx) |
||||
|
||||
if err := wc.Close(); err != nil { |
||||
err = u.uploadFile(client, imageDiskPath, key) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
return fmt.Sprintf("https://storage.googleapis.com/%s/%s", u.bucket, key), nil |
||||
} |
||||
|
||||
func (u *GCSUploader) uploadFile(client *http.Client, imageDiskPath, key string) error { |
||||
log.Debug("Opening image file ", imageDiskPath) |
||||
|
||||
fileReader, err := os.Open(imageDiskPath) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
reqUrl := fmt.Sprintf( |
||||
"https://www.googleapis.com/upload/storage/v1/b/%s/o?uploadType=media&name=%s&predefinedAcl=publicRead", |
||||
u.bucket, |
||||
key, |
||||
) |
||||
|
||||
log.Debug("Request URL: ", reqUrl) |
||||
|
||||
req, err := http.NewRequest("POST", reqUrl, fileReader) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
req.Header.Add("Content-Type", "image/png") |
||||
|
||||
log.Debug("Sending POST request to GCS") |
||||
|
||||
resp, err := client.Do(req) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
log.Debug("GCS API response header", resp.Header) |
||||
|
||||
if resp.StatusCode != 200 { |
||||
return errors.New(fmt.Sprintf("GCS response status code %d", resp.StatusCode)) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
@ -0,0 +1,437 @@ |
||||
// Copyright 2014 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package metadata provides access to Google Compute Engine (GCE)
|
||||
// metadata and API service accounts.
|
||||
//
|
||||
// This package is a wrapper around the GCE metadata service,
|
||||
// as documented at https://developers.google.com/compute/docs/metadata.
|
||||
package metadata // import "cloud.google.com/go/compute/metadata"
|
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"net" |
||||
"net/http" |
||||
"net/url" |
||||
"os" |
||||
"runtime" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"golang.org/x/net/context" |
||||
"golang.org/x/net/context/ctxhttp" |
||||
) |
||||
|
||||
const ( |
||||
// metadataIP is the documented metadata server IP address.
|
||||
metadataIP = "169.254.169.254" |
||||
|
||||
// metadataHostEnv is the environment variable specifying the
|
||||
// GCE metadata hostname. If empty, the default value of
|
||||
// metadataIP ("169.254.169.254") is used instead.
|
||||
// This is variable name is not defined by any spec, as far as
|
||||
// I know; it was made up for the Go package.
|
||||
metadataHostEnv = "GCE_METADATA_HOST" |
||||
|
||||
userAgent = "gcloud-golang/0.1" |
||||
) |
||||
|
||||
type cachedValue struct { |
||||
k string |
||||
trim bool |
||||
mu sync.Mutex |
||||
v string |
||||
} |
||||
|
||||
var ( |
||||
projID = &cachedValue{k: "project/project-id", trim: true} |
||||
projNum = &cachedValue{k: "project/numeric-project-id", trim: true} |
||||
instID = &cachedValue{k: "instance/id", trim: true} |
||||
) |
||||
|
||||
var ( |
||||
metaClient = &http.Client{ |
||||
Transport: &http.Transport{ |
||||
Dial: (&net.Dialer{ |
||||
Timeout: 2 * time.Second, |
||||
KeepAlive: 30 * time.Second, |
||||
}).Dial, |
||||
ResponseHeaderTimeout: 2 * time.Second, |
||||
}, |
||||
} |
||||
subscribeClient = &http.Client{ |
||||
Transport: &http.Transport{ |
||||
Dial: (&net.Dialer{ |
||||
Timeout: 2 * time.Second, |
||||
KeepAlive: 30 * time.Second, |
||||
}).Dial, |
||||
}, |
||||
} |
||||
) |
||||
|
||||
// NotDefinedError is returned when requested metadata is not defined.
|
||||
//
|
||||
// The underlying string is the suffix after "/computeMetadata/v1/".
|
||||
//
|
||||
// This error is not returned if the value is defined to be the empty
|
||||
// string.
|
||||
type NotDefinedError string |
||||
|
||||
func (suffix NotDefinedError) Error() string { |
||||
return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix)) |
||||
} |
||||
|
||||
// Get returns a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
//
|
||||
// If the GCE_METADATA_HOST environment variable is not defined, a default of
|
||||
// 169.254.169.254 will be used instead.
|
||||
//
|
||||
// If the requested metadata is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
func Get(suffix string) (string, error) { |
||||
val, _, err := getETag(metaClient, suffix) |
||||
return val, err |
||||
} |
||||
|
||||
// getETag returns a value from the metadata service as well as the associated
|
||||
// ETag using the provided client. This func is otherwise equivalent to Get.
|
||||
func getETag(client *http.Client, suffix string) (value, etag string, err error) { |
||||
// Using a fixed IP makes it very difficult to spoof the metadata service in
|
||||
// a container, which is an important use-case for local testing of cloud
|
||||
// deployments. To enable spoofing of the metadata service, the environment
|
||||
// variable GCE_METADATA_HOST is first inspected to decide where metadata
|
||||
// requests shall go.
|
||||
host := os.Getenv(metadataHostEnv) |
||||
if host == "" { |
||||
// Using 169.254.169.254 instead of "metadata" here because Go
|
||||
// binaries built with the "netgo" tag and without cgo won't
|
||||
// know the search suffix for "metadata" is
|
||||
// ".google.internal", and this IP address is documented as
|
||||
// being stable anyway.
|
||||
host = metadataIP |
||||
} |
||||
url := "http://" + host + "/computeMetadata/v1/" + suffix |
||||
req, _ := http.NewRequest("GET", url, nil) |
||||
req.Header.Set("Metadata-Flavor", "Google") |
||||
req.Header.Set("User-Agent", userAgent) |
||||
res, err := client.Do(req) |
||||
if err != nil { |
||||
return "", "", err |
||||
} |
||||
defer res.Body.Close() |
||||
if res.StatusCode == http.StatusNotFound { |
||||
return "", "", NotDefinedError(suffix) |
||||
} |
||||
if res.StatusCode != 200 { |
||||
return "", "", fmt.Errorf("status code %d trying to fetch %s", res.StatusCode, url) |
||||
} |
||||
all, err := ioutil.ReadAll(res.Body) |
||||
if err != nil { |
||||
return "", "", err |
||||
} |
||||
return string(all), res.Header.Get("Etag"), nil |
||||
} |
||||
|
||||
func getTrimmed(suffix string) (s string, err error) { |
||||
s, err = Get(suffix) |
||||
s = strings.TrimSpace(s) |
||||
return |
||||
} |
||||
|
||||
func (c *cachedValue) get() (v string, err error) { |
||||
defer c.mu.Unlock() |
||||
c.mu.Lock() |
||||
if c.v != "" { |
||||
return c.v, nil |
||||
} |
||||
if c.trim { |
||||
v, err = getTrimmed(c.k) |
||||
} else { |
||||
v, err = Get(c.k) |
||||
} |
||||
if err == nil { |
||||
c.v = v |
||||
} |
||||
return |
||||
} |
||||
|
||||
var ( |
||||
onGCEOnce sync.Once |
||||
onGCE bool |
||||
) |
||||
|
||||
// OnGCE reports whether this process is running on Google Compute Engine.
|
||||
func OnGCE() bool { |
||||
onGCEOnce.Do(initOnGCE) |
||||
return onGCE |
||||
} |
||||
|
||||
func initOnGCE() { |
||||
onGCE = testOnGCE() |
||||
} |
||||
|
||||
func testOnGCE() bool { |
||||
// The user explicitly said they're on GCE, so trust them.
|
||||
if os.Getenv(metadataHostEnv) != "" { |
||||
return true |
||||
} |
||||
|
||||
ctx, cancel := context.WithCancel(context.Background()) |
||||
defer cancel() |
||||
|
||||
resc := make(chan bool, 2) |
||||
|
||||
// Try two strategies in parallel.
|
||||
// See https://github.com/GoogleCloudPlatform/google-cloud-go/issues/194
|
||||
go func() { |
||||
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil) |
||||
req.Header.Set("User-Agent", userAgent) |
||||
res, err := ctxhttp.Do(ctx, metaClient, req) |
||||
if err != nil { |
||||
resc <- false |
||||
return |
||||
} |
||||
defer res.Body.Close() |
||||
resc <- res.Header.Get("Metadata-Flavor") == "Google" |
||||
}() |
||||
|
||||
go func() { |
||||
addrs, err := net.LookupHost("metadata.google.internal") |
||||
if err != nil || len(addrs) == 0 { |
||||
resc <- false |
||||
return |
||||
} |
||||
resc <- strsContains(addrs, metadataIP) |
||||
}() |
||||
|
||||
tryHarder := systemInfoSuggestsGCE() |
||||
if tryHarder { |
||||
res := <-resc |
||||
if res { |
||||
// The first strategy succeeded, so let's use it.
|
||||
return true |
||||
} |
||||
// Wait for either the DNS or metadata server probe to
|
||||
// contradict the other one and say we are running on
|
||||
// GCE. Give it a lot of time to do so, since the system
|
||||
// info already suggests we're running on a GCE BIOS.
|
||||
timer := time.NewTimer(5 * time.Second) |
||||
defer timer.Stop() |
||||
select { |
||||
case res = <-resc: |
||||
return res |
||||
case <-timer.C: |
||||
// Too slow. Who knows what this system is.
|
||||
return false |
||||
} |
||||
} |
||||
|
||||
// There's no hint from the system info that we're running on
|
||||
// GCE, so use the first probe's result as truth, whether it's
|
||||
// true or false. The goal here is to optimize for speed for
|
||||
// users who are NOT running on GCE. We can't assume that
|
||||
// either a DNS lookup or an HTTP request to a blackholed IP
|
||||
// address is fast. Worst case this should return when the
|
||||
// metaClient's Transport.ResponseHeaderTimeout or
|
||||
// Transport.Dial.Timeout fires (in two seconds).
|
||||
return <-resc |
||||
} |
||||
|
||||
// systemInfoSuggestsGCE reports whether the local system (without
|
||||
// doing network requests) suggests that we're running on GCE. If this
|
||||
// returns true, testOnGCE tries a bit harder to reach its metadata
|
||||
// server.
|
||||
func systemInfoSuggestsGCE() bool { |
||||
if runtime.GOOS != "linux" { |
||||
// We don't have any non-Linux clues available, at least yet.
|
||||
return false |
||||
} |
||||
slurp, _ := ioutil.ReadFile("/sys/class/dmi/id/product_name") |
||||
name := strings.TrimSpace(string(slurp)) |
||||
return name == "Google" || name == "Google Compute Engine" |
||||
} |
||||
|
||||
// Subscribe subscribes to a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
// The suffix may contain query parameters.
|
||||
//
|
||||
// Subscribe calls fn with the latest metadata value indicated by the provided
|
||||
// suffix. If the metadata value is deleted, fn is called with the empty string
|
||||
// and ok false. Subscribe blocks until fn returns a non-nil error or the value
|
||||
// is deleted. Subscribe returns the error value returned from the last call to
|
||||
// fn, which may be nil when ok == false.
|
||||
func Subscribe(suffix string, fn func(v string, ok bool) error) error { |
||||
const failedSubscribeSleep = time.Second * 5 |
||||
|
||||
// First check to see if the metadata value exists at all.
|
||||
val, lastETag, err := getETag(subscribeClient, suffix) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := fn(val, true); err != nil { |
||||
return err |
||||
} |
||||
|
||||
ok := true |
||||
if strings.ContainsRune(suffix, '?') { |
||||
suffix += "&wait_for_change=true&last_etag=" |
||||
} else { |
||||
suffix += "?wait_for_change=true&last_etag=" |
||||
} |
||||
for { |
||||
val, etag, err := getETag(subscribeClient, suffix+url.QueryEscape(lastETag)) |
||||
if err != nil { |
||||
if _, deleted := err.(NotDefinedError); !deleted { |
||||
time.Sleep(failedSubscribeSleep) |
||||
continue // Retry on other errors.
|
||||
} |
||||
ok = false |
||||
} |
||||
lastETag = etag |
||||
|
||||
if err := fn(val, ok); err != nil || !ok { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ProjectID returns the current instance's project ID string.
|
||||
func ProjectID() (string, error) { return projID.get() } |
||||
|
||||
// NumericProjectID returns the current instance's numeric project ID.
|
||||
func NumericProjectID() (string, error) { return projNum.get() } |
||||
|
||||
// InternalIP returns the instance's primary internal IP address.
|
||||
func InternalIP() (string, error) { |
||||
return getTrimmed("instance/network-interfaces/0/ip") |
||||
} |
||||
|
||||
// ExternalIP returns the instance's primary external (public) IP address.
|
||||
func ExternalIP() (string, error) { |
||||
return getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip") |
||||
} |
||||
|
||||
// Hostname returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
func Hostname() (string, error) { |
||||
return getTrimmed("instance/hostname") |
||||
} |
||||
|
||||
// InstanceTags returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func InstanceTags() ([]string, error) { |
||||
var s []string |
||||
j, err := Get("instance/tags") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil { |
||||
return nil, err |
||||
} |
||||
return s, nil |
||||
} |
||||
|
||||
// InstanceID returns the current VM's numeric instance ID.
|
||||
func InstanceID() (string, error) { |
||||
return instID.get() |
||||
} |
||||
|
||||
// InstanceName returns the current VM's instance ID string.
|
||||
func InstanceName() (string, error) { |
||||
host, err := Hostname() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return strings.Split(host, ".")[0], nil |
||||
} |
||||
|
||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||
func Zone() (string, error) { |
||||
zone, err := getTrimmed("instance/zone") |
||||
// zone is of the form "projects/<projNum>/zones/<zoneName>".
|
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return zone[strings.LastIndex(zone, "/")+1:], nil |
||||
} |
||||
|
||||
// InstanceAttributes returns the list of user-defined attributes,
|
||||
// assigned when initially creating a GCE VM instance. The value of an
|
||||
// attribute can be obtained with InstanceAttributeValue.
|
||||
func InstanceAttributes() ([]string, error) { return lines("instance/attributes/") } |
||||
|
||||
// ProjectAttributes returns the list of user-defined attributes
|
||||
// applying to the project as a whole, not just this VM. The value of
|
||||
// an attribute can be obtained with ProjectAttributeValue.
|
||||
func ProjectAttributes() ([]string, error) { return lines("project/attributes/") } |
||||
|
||||
func lines(suffix string) ([]string, error) { |
||||
j, err := Get(suffix) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
s := strings.Split(strings.TrimSpace(j), "\n") |
||||
for i := range s { |
||||
s[i] = strings.TrimSpace(s[i]) |
||||
} |
||||
return s, nil |
||||
} |
||||
|
||||
// InstanceAttributeValue returns the value of the provided VM
|
||||
// instance attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// InstanceAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func InstanceAttributeValue(attr string) (string, error) { |
||||
return Get("instance/attributes/" + attr) |
||||
} |
||||
|
||||
// ProjectAttributeValue returns the value of the provided
|
||||
// project attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// ProjectAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func ProjectAttributeValue(attr string) (string, error) { |
||||
return Get("project/attributes/" + attr) |
||||
} |
||||
|
||||
// Scopes returns the service account scopes for the given account.
|
||||
// The account may be empty or the string "default" to use the instance's
|
||||
// main account.
|
||||
func Scopes(serviceAccount string) ([]string, error) { |
||||
if serviceAccount == "" { |
||||
serviceAccount = "default" |
||||
} |
||||
return lines("instance/service-accounts/" + serviceAccount + "/scopes") |
||||
} |
||||
|
||||
func strsContains(ss []string, s string) bool { |
||||
for _, v := range ss { |
||||
if v == s { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
@ -0,0 +1,89 @@ |
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package google |
||||
|
||||
import ( |
||||
"sort" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"golang.org/x/net/context" |
||||
"golang.org/x/oauth2" |
||||
) |
||||
|
||||
// appengineFlex is set at init time by appengineflex_hook.go. If true, we are on App Engine Flex.
|
||||
var appengineFlex bool |
||||
|
||||
// Set at init time by appengine_hook.go. If nil, we're not on App Engine.
|
||||
var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error) |
||||
|
||||
// Set at init time by appengine_hook.go. If nil, we're not on App Engine.
|
||||
var appengineAppIDFunc func(c context.Context) string |
||||
|
||||
// AppEngineTokenSource returns a token source that fetches tokens
|
||||
// issued to the current App Engine application's service account.
|
||||
// If you are implementing a 3-legged OAuth 2.0 flow on App Engine
|
||||
// that involves user accounts, see oauth2.Config instead.
|
||||
//
|
||||
// The provided context must have come from appengine.NewContext.
|
||||
func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource { |
||||
if appengineTokenFunc == nil { |
||||
panic("google: AppEngineTokenSource can only be used on App Engine.") |
||||
} |
||||
scopes := append([]string{}, scope...) |
||||
sort.Strings(scopes) |
||||
return &appEngineTokenSource{ |
||||
ctx: ctx, |
||||
scopes: scopes, |
||||
key: strings.Join(scopes, " "), |
||||
} |
||||
} |
||||
|
||||
// aeTokens helps the fetched tokens to be reused until their expiration.
|
||||
var ( |
||||
aeTokensMu sync.Mutex |
||||
aeTokens = make(map[string]*tokenLock) // key is space-separated scopes
|
||||
) |
||||
|
||||
type tokenLock struct { |
||||
mu sync.Mutex // guards t; held while fetching or updating t
|
||||
t *oauth2.Token |
||||
} |
||||
|
||||
type appEngineTokenSource struct { |
||||
ctx context.Context |
||||
scopes []string |
||||
key string // to aeTokens map; space-separated scopes
|
||||
} |
||||
|
||||
func (ts *appEngineTokenSource) Token() (*oauth2.Token, error) { |
||||
if appengineTokenFunc == nil { |
||||
panic("google: AppEngineTokenSource can only be used on App Engine.") |
||||
} |
||||
|
||||
aeTokensMu.Lock() |
||||
tok, ok := aeTokens[ts.key] |
||||
if !ok { |
||||
tok = &tokenLock{} |
||||
aeTokens[ts.key] = tok |
||||
} |
||||
aeTokensMu.Unlock() |
||||
|
||||
tok.mu.Lock() |
||||
defer tok.mu.Unlock() |
||||
if tok.t.Valid() { |
||||
return tok.t, nil |
||||
} |
||||
access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
tok.t = &oauth2.Token{ |
||||
AccessToken: access, |
||||
Expiry: exp, |
||||
} |
||||
return tok.t, nil |
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build appengine appenginevm
|
||||
|
||||
package google |
||||
|
||||
import "google.golang.org/appengine" |
||||
|
||||
func init() { |
||||
appengineTokenFunc = appengine.AccessToken |
||||
appengineAppIDFunc = appengine.AppID |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build appenginevm
|
||||
|
||||
package google |
||||
|
||||
func init() { |
||||
appengineFlex = true // Flex doesn't support appengine.AccessToken; depend on metadata server.
|
||||
} |
||||
@ -0,0 +1,130 @@ |
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package google |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"os" |
||||
"path/filepath" |
||||
"runtime" |
||||
|
||||
"cloud.google.com/go/compute/metadata" |
||||
"golang.org/x/net/context" |
||||
"golang.org/x/oauth2" |
||||
) |
||||
|
||||
// DefaultCredentials holds "Application Default Credentials".
|
||||
// For more details, see:
|
||||
// https://developers.google.com/accounts/docs/application-default-credentials
|
||||
type DefaultCredentials struct { |
||||
ProjectID string // may be empty
|
||||
TokenSource oauth2.TokenSource |
||||
} |
||||
|
||||
// DefaultClient returns an HTTP Client that uses the
|
||||
// DefaultTokenSource to obtain authentication credentials.
|
||||
func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) { |
||||
ts, err := DefaultTokenSource(ctx, scope...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return oauth2.NewClient(ctx, ts), nil |
||||
} |
||||
|
||||
// DefaultTokenSource returns the token source for
|
||||
// "Application Default Credentials".
|
||||
// It is a shortcut for FindDefaultCredentials(ctx, scope).TokenSource.
|
||||
func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSource, error) { |
||||
creds, err := FindDefaultCredentials(ctx, scope...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return creds.TokenSource, nil |
||||
} |
||||
|
||||
// FindDefaultCredentials searches for "Application Default Credentials".
|
||||
//
|
||||
// It looks for credentials in the following places,
|
||||
// preferring the first location found:
|
||||
//
|
||||
// 1. A JSON file whose path is specified by the
|
||||
// GOOGLE_APPLICATION_CREDENTIALS environment variable.
|
||||
// 2. A JSON file in a location known to the gcloud command-line tool.
|
||||
// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
|
||||
// On other systems, $HOME/.config/gcloud/application_default_credentials.json.
|
||||
// 3. On Google App Engine it uses the appengine.AccessToken function.
|
||||
// 4. On Google Compute Engine and Google App Engine Managed VMs, it fetches
|
||||
// credentials from the metadata server.
|
||||
// (In this final case any provided scopes are ignored.)
|
||||
func FindDefaultCredentials(ctx context.Context, scope ...string) (*DefaultCredentials, error) { |
||||
// First, try the environment variable.
|
||||
const envVar = "GOOGLE_APPLICATION_CREDENTIALS" |
||||
if filename := os.Getenv(envVar); filename != "" { |
||||
creds, err := readCredentialsFile(ctx, filename, scope) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("google: error getting credentials using %v environment variable: %v", envVar, err) |
||||
} |
||||
return creds, nil |
||||
} |
||||
|
||||
// Second, try a well-known file.
|
||||
filename := wellKnownFile() |
||||
if creds, err := readCredentialsFile(ctx, filename, scope); err == nil { |
||||
return creds, nil |
||||
} else if !os.IsNotExist(err) { |
||||
return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err) |
||||
} |
||||
|
||||
// Third, if we're on Google App Engine use those credentials.
|
||||
if appengineTokenFunc != nil && !appengineFlex { |
||||
return &DefaultCredentials{ |
||||
ProjectID: appengineAppIDFunc(ctx), |
||||
TokenSource: AppEngineTokenSource(ctx, scope...), |
||||
}, nil |
||||
} |
||||
|
||||
// Fourth, if we're on Google Compute Engine use the metadata server.
|
||||
if metadata.OnGCE() { |
||||
id, _ := metadata.ProjectID() |
||||
return &DefaultCredentials{ |
||||
ProjectID: id, |
||||
TokenSource: ComputeTokenSource(""), |
||||
}, nil |
||||
} |
||||
|
||||
// None are found; return helpful error.
|
||||
const url = "https://developers.google.com/accounts/docs/application-default-credentials" |
||||
return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url) |
||||
} |
||||
|
||||
func wellKnownFile() string { |
||||
const f = "application_default_credentials.json" |
||||
if runtime.GOOS == "windows" { |
||||
return filepath.Join(os.Getenv("APPDATA"), "gcloud", f) |
||||
} |
||||
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f) |
||||
} |
||||
|
||||
func readCredentialsFile(ctx context.Context, filename string, scopes []string) (*DefaultCredentials, error) { |
||||
b, err := ioutil.ReadFile(filename) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var f credentialsFile |
||||
if err := json.Unmarshal(b, &f); err != nil { |
||||
return nil, err |
||||
} |
||||
ts, err := f.tokenSource(ctx, append([]string(nil), scopes...)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &DefaultCredentials{ |
||||
ProjectID: f.ProjectID, |
||||
TokenSource: ts, |
||||
}, nil |
||||
} |
||||
@ -0,0 +1,202 @@ |
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package google provides support for making OAuth2 authorized and
|
||||
// authenticated HTTP requests to Google APIs.
|
||||
// It supports the Web server flow, client-side credentials, service accounts,
|
||||
// Google Compute Engine service accounts, and Google App Engine service
|
||||
// accounts.
|
||||
//
|
||||
// For more information, please read
|
||||
// https://developers.google.com/accounts/docs/OAuth2
|
||||
// and
|
||||
// https://developers.google.com/accounts/docs/application-default-credentials.
|
||||
package google // import "golang.org/x/oauth2/google"
|
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"strings" |
||||
"time" |
||||
|
||||
"cloud.google.com/go/compute/metadata" |
||||
"golang.org/x/net/context" |
||||
"golang.org/x/oauth2" |
||||
"golang.org/x/oauth2/jwt" |
||||
) |
||||
|
||||
// Endpoint is Google's OAuth 2.0 endpoint.
|
||||
var Endpoint = oauth2.Endpoint{ |
||||
AuthURL: "https://accounts.google.com/o/oauth2/auth", |
||||
TokenURL: "https://accounts.google.com/o/oauth2/token", |
||||
} |
||||
|
||||
// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
|
||||
const JWTTokenURL = "https://accounts.google.com/o/oauth2/token" |
||||
|
||||
// ConfigFromJSON uses a Google Developers Console client_credentials.json
|
||||
// file to construct a config.
|
||||
// client_credentials.json can be downloaded from
|
||||
// https://console.developers.google.com, under "Credentials". Download the Web
|
||||
// application credentials in the JSON format and provide the contents of the
|
||||
// file as jsonKey.
|
||||
func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) { |
||||
type cred struct { |
||||
ClientID string `json:"client_id"` |
||||
ClientSecret string `json:"client_secret"` |
||||
RedirectURIs []string `json:"redirect_uris"` |
||||
AuthURI string `json:"auth_uri"` |
||||
TokenURI string `json:"token_uri"` |
||||
} |
||||
var j struct { |
||||
Web *cred `json:"web"` |
||||
Installed *cred `json:"installed"` |
||||
} |
||||
if err := json.Unmarshal(jsonKey, &j); err != nil { |
||||
return nil, err |
||||
} |
||||
var c *cred |
||||
switch { |
||||
case j.Web != nil: |
||||
c = j.Web |
||||
case j.Installed != nil: |
||||
c = j.Installed |
||||
default: |
||||
return nil, fmt.Errorf("oauth2/google: no credentials found") |
||||
} |
||||
if len(c.RedirectURIs) < 1 { |
||||
return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json") |
||||
} |
||||
return &oauth2.Config{ |
||||
ClientID: c.ClientID, |
||||
ClientSecret: c.ClientSecret, |
||||
RedirectURL: c.RedirectURIs[0], |
||||
Scopes: scope, |
||||
Endpoint: oauth2.Endpoint{ |
||||
AuthURL: c.AuthURI, |
||||
TokenURL: c.TokenURI, |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
// JWTConfigFromJSON uses a Google Developers service account JSON key file to read
|
||||
// the credentials that authorize and authenticate the requests.
|
||||
// Create a service account on "Credentials" for your project at
|
||||
// https://console.developers.google.com to download a JSON key file.
|
||||
func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) { |
||||
var f credentialsFile |
||||
if err := json.Unmarshal(jsonKey, &f); err != nil { |
||||
return nil, err |
||||
} |
||||
if f.Type != serviceAccountKey { |
||||
return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey) |
||||
} |
||||
scope = append([]string(nil), scope...) // copy
|
||||
return f.jwtConfig(scope), nil |
||||
} |
||||
|
||||
// JSON key file types.
|
||||
const ( |
||||
serviceAccountKey = "service_account" |
||||
userCredentialsKey = "authorized_user" |
||||
) |
||||
|
||||
// credentialsFile is the unmarshalled representation of a credentials file.
|
||||
type credentialsFile struct { |
||||
Type string `json:"type"` // serviceAccountKey or userCredentialsKey
|
||||
|
||||
// Service Account fields
|
||||
ClientEmail string `json:"client_email"` |
||||
PrivateKeyID string `json:"private_key_id"` |
||||
PrivateKey string `json:"private_key"` |
||||
TokenURL string `json:"token_uri"` |
||||
ProjectID string `json:"project_id"` |
||||
|
||||
// User Credential fields
|
||||
// (These typically come from gcloud auth.)
|
||||
ClientSecret string `json:"client_secret"` |
||||
ClientID string `json:"client_id"` |
||||
RefreshToken string `json:"refresh_token"` |
||||
} |
||||
|
||||
func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config { |
||||
cfg := &jwt.Config{ |
||||
Email: f.ClientEmail, |
||||
PrivateKey: []byte(f.PrivateKey), |
||||
PrivateKeyID: f.PrivateKeyID, |
||||
Scopes: scopes, |
||||
TokenURL: f.TokenURL, |
||||
} |
||||
if cfg.TokenURL == "" { |
||||
cfg.TokenURL = JWTTokenURL |
||||
} |
||||
return cfg |
||||
} |
||||
|
||||
func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oauth2.TokenSource, error) { |
||||
switch f.Type { |
||||
case serviceAccountKey: |
||||
cfg := f.jwtConfig(scopes) |
||||
return cfg.TokenSource(ctx), nil |
||||
case userCredentialsKey: |
||||
cfg := &oauth2.Config{ |
||||
ClientID: f.ClientID, |
||||
ClientSecret: f.ClientSecret, |
||||
Scopes: scopes, |
||||
Endpoint: Endpoint, |
||||
} |
||||
tok := &oauth2.Token{RefreshToken: f.RefreshToken} |
||||
return cfg.TokenSource(ctx, tok), nil |
||||
case "": |
||||
return nil, errors.New("missing 'type' field in credentials") |
||||
default: |
||||
return nil, fmt.Errorf("unknown credential type: %q", f.Type) |
||||
} |
||||
} |
||||
|
||||
// ComputeTokenSource returns a token source that fetches access tokens
|
||||
// from Google Compute Engine (GCE)'s metadata server. It's only valid to use
|
||||
// this token source if your program is running on a GCE instance.
|
||||
// If no account is specified, "default" is used.
|
||||
// Further information about retrieving access tokens from the GCE metadata
|
||||
// server can be found at https://cloud.google.com/compute/docs/authentication.
|
||||
func ComputeTokenSource(account string) oauth2.TokenSource { |
||||
return oauth2.ReuseTokenSource(nil, computeSource{account: account}) |
||||
} |
||||
|
||||
type computeSource struct { |
||||
account string |
||||
} |
||||
|
||||
func (cs computeSource) Token() (*oauth2.Token, error) { |
||||
if !metadata.OnGCE() { |
||||
return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE") |
||||
} |
||||
acct := cs.account |
||||
if acct == "" { |
||||
acct = "default" |
||||
} |
||||
tokenJSON, err := metadata.Get("instance/service-accounts/" + acct + "/token") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var res struct { |
||||
AccessToken string `json:"access_token"` |
||||
ExpiresInSec int `json:"expires_in"` |
||||
TokenType string `json:"token_type"` |
||||
} |
||||
err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err) |
||||
} |
||||
if res.ExpiresInSec == 0 || res.AccessToken == "" { |
||||
return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata") |
||||
} |
||||
return &oauth2.Token{ |
||||
AccessToken: res.AccessToken, |
||||
TokenType: res.TokenType, |
||||
Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second), |
||||
}, nil |
||||
} |
||||
@ -0,0 +1,74 @@ |
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package google |
||||
|
||||
import ( |
||||
"crypto/rsa" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"golang.org/x/oauth2" |
||||
"golang.org/x/oauth2/internal" |
||||
"golang.org/x/oauth2/jws" |
||||
) |
||||
|
||||
// JWTAccessTokenSourceFromJSON uses a Google Developers service account JSON
|
||||
// key file to read the credentials that authorize and authenticate the
|
||||
// requests, and returns a TokenSource that does not use any OAuth2 flow but
|
||||
// instead creates a JWT and sends that as the access token.
|
||||
// The audience is typically a URL that specifies the scope of the credentials.
|
||||
//
|
||||
// Note that this is not a standard OAuth flow, but rather an
|
||||
// optimization supported by a few Google services.
|
||||
// Unless you know otherwise, you should use JWTConfigFromJSON instead.
|
||||
func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.TokenSource, error) { |
||||
cfg, err := JWTConfigFromJSON(jsonKey) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("google: could not parse JSON key: %v", err) |
||||
} |
||||
pk, err := internal.ParseKey(cfg.PrivateKey) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("google: could not parse key: %v", err) |
||||
} |
||||
ts := &jwtAccessTokenSource{ |
||||
email: cfg.Email, |
||||
audience: audience, |
||||
pk: pk, |
||||
pkID: cfg.PrivateKeyID, |
||||
} |
||||
tok, err := ts.Token() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return oauth2.ReuseTokenSource(tok, ts), nil |
||||
} |
||||
|
||||
type jwtAccessTokenSource struct { |
||||
email, audience string |
||||
pk *rsa.PrivateKey |
||||
pkID string |
||||
} |
||||
|
||||
func (ts *jwtAccessTokenSource) Token() (*oauth2.Token, error) { |
||||
iat := time.Now() |
||||
exp := iat.Add(time.Hour) |
||||
cs := &jws.ClaimSet{ |
||||
Iss: ts.email, |
||||
Sub: ts.email, |
||||
Aud: ts.audience, |
||||
Iat: iat.Unix(), |
||||
Exp: exp.Unix(), |
||||
} |
||||
hdr := &jws.Header{ |
||||
Algorithm: "RS256", |
||||
Typ: "JWT", |
||||
KeyID: string(ts.pkID), |
||||
} |
||||
msg, err := jws.Encode(hdr, cs, ts.pk) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("google: could not encode JWT: %v", err) |
||||
} |
||||
return &oauth2.Token{AccessToken: msg, TokenType: "Bearer", Expiry: exp}, nil |
||||
} |
||||
@ -0,0 +1,172 @@ |
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package google |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"net/http" |
||||
"os" |
||||
"os/user" |
||||
"path/filepath" |
||||
"runtime" |
||||
"strings" |
||||
"time" |
||||
|
||||
"golang.org/x/net/context" |
||||
"golang.org/x/oauth2" |
||||
"golang.org/x/oauth2/internal" |
||||
) |
||||
|
||||
type sdkCredentials struct { |
||||
Data []struct { |
||||
Credential struct { |
||||
ClientID string `json:"client_id"` |
||||
ClientSecret string `json:"client_secret"` |
||||
AccessToken string `json:"access_token"` |
||||
RefreshToken string `json:"refresh_token"` |
||||
TokenExpiry *time.Time `json:"token_expiry"` |
||||
} `json:"credential"` |
||||
Key struct { |
||||
Account string `json:"account"` |
||||
Scope string `json:"scope"` |
||||
} `json:"key"` |
||||
} |
||||
} |
||||
|
||||
// An SDKConfig provides access to tokens from an account already
|
||||
// authorized via the Google Cloud SDK.
|
||||
type SDKConfig struct { |
||||
conf oauth2.Config |
||||
initialToken *oauth2.Token |
||||
} |
||||
|
||||
// NewSDKConfig creates an SDKConfig for the given Google Cloud SDK
|
||||
// account. If account is empty, the account currently active in
|
||||
// Google Cloud SDK properties is used.
|
||||
// Google Cloud SDK credentials must be created by running `gcloud auth`
|
||||
// before using this function.
|
||||
// The Google Cloud SDK is available at https://cloud.google.com/sdk/.
|
||||
func NewSDKConfig(account string) (*SDKConfig, error) { |
||||
configPath, err := sdkConfigPath() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err) |
||||
} |
||||
credentialsPath := filepath.Join(configPath, "credentials") |
||||
f, err := os.Open(credentialsPath) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err) |
||||
} |
||||
defer f.Close() |
||||
|
||||
var c sdkCredentials |
||||
if err := json.NewDecoder(f).Decode(&c); err != nil { |
||||
return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err) |
||||
} |
||||
if len(c.Data) == 0 { |
||||
return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath) |
||||
} |
||||
if account == "" { |
||||
propertiesPath := filepath.Join(configPath, "properties") |
||||
f, err := os.Open(propertiesPath) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err) |
||||
} |
||||
defer f.Close() |
||||
ini, err := internal.ParseINI(f) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err) |
||||
} |
||||
core, ok := ini["core"] |
||||
if !ok { |
||||
return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini) |
||||
} |
||||
active, ok := core["account"] |
||||
if !ok { |
||||
return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core) |
||||
} |
||||
account = active |
||||
} |
||||
|
||||
for _, d := range c.Data { |
||||
if account == "" || d.Key.Account == account { |
||||
if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" { |
||||
return nil, fmt.Errorf("oauth2/google: no token available for account %q", account) |
||||
} |
||||
var expiry time.Time |
||||
if d.Credential.TokenExpiry != nil { |
||||
expiry = *d.Credential.TokenExpiry |
||||
} |
||||
return &SDKConfig{ |
||||
conf: oauth2.Config{ |
||||
ClientID: d.Credential.ClientID, |
||||
ClientSecret: d.Credential.ClientSecret, |
||||
Scopes: strings.Split(d.Key.Scope, " "), |
||||
Endpoint: Endpoint, |
||||
RedirectURL: "oob", |
||||
}, |
||||
initialToken: &oauth2.Token{ |
||||
AccessToken: d.Credential.AccessToken, |
||||
RefreshToken: d.Credential.RefreshToken, |
||||
Expiry: expiry, |
||||
}, |
||||
}, nil |
||||
} |
||||
} |
||||
return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account) |
||||
} |
||||
|
||||
// Client returns an HTTP client using Google Cloud SDK credentials to
|
||||
// authorize requests. The token will auto-refresh as necessary. The
|
||||
// underlying http.RoundTripper will be obtained using the provided
|
||||
// context. The returned client and its Transport should not be
|
||||
// modified.
|
||||
func (c *SDKConfig) Client(ctx context.Context) *http.Client { |
||||
return &http.Client{ |
||||
Transport: &oauth2.Transport{ |
||||
Source: c.TokenSource(ctx), |
||||
}, |
||||
} |
||||
} |
||||
|
||||
// TokenSource returns an oauth2.TokenSource that retrieve tokens from
|
||||
// Google Cloud SDK credentials using the provided context.
|
||||
// It will returns the current access token stored in the credentials,
|
||||
// and refresh it when it expires, but it won't update the credentials
|
||||
// with the new access token.
|
||||
func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource { |
||||
return c.conf.TokenSource(ctx, c.initialToken) |
||||
} |
||||
|
||||
// Scopes are the OAuth 2.0 scopes the current account is authorized for.
|
||||
func (c *SDKConfig) Scopes() []string { |
||||
return c.conf.Scopes |
||||
} |
||||
|
||||
// sdkConfigPath tries to guess where the gcloud config is located.
|
||||
// It can be overridden during tests.
|
||||
var sdkConfigPath = func() (string, error) { |
||||
if runtime.GOOS == "windows" { |
||||
return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil |
||||
} |
||||
homeDir := guessUnixHomeDir() |
||||
if homeDir == "" { |
||||
return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty") |
||||
} |
||||
return filepath.Join(homeDir, ".config", "gcloud"), nil |
||||
} |
||||
|
||||
func guessUnixHomeDir() string { |
||||
// Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470
|
||||
if v := os.Getenv("HOME"); v != "" { |
||||
return v |
||||
} |
||||
// Else, fall back to user.Current:
|
||||
if u, err := user.Current(); err == nil { |
||||
return u.HomeDir |
||||
} |
||||
return "" |
||||
} |
||||
@ -0,0 +1,182 @@ |
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package jws provides a partial implementation
|
||||
// of JSON Web Signature encoding and decoding.
|
||||
// It exists to support the golang.org/x/oauth2 package.
|
||||
//
|
||||
// See RFC 7515.
|
||||
//
|
||||
// Deprecated: this package is not intended for public use and might be
|
||||
// removed in the future. It exists for internal use only.
|
||||
// Please switch to another JWS package or copy this package into your own
|
||||
// source tree.
|
||||
package jws // import "golang.org/x/oauth2/jws"
|
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto" |
||||
"crypto/rand" |
||||
"crypto/rsa" |
||||
"crypto/sha256" |
||||
"encoding/base64" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
// ClaimSet contains information about the JWT signature including the
|
||||
// permissions being requested (scopes), the target of the token, the issuer,
|
||||
// the time the token was issued, and the lifetime of the token.
|
||||
type ClaimSet struct { |
||||
Iss string `json:"iss"` // email address of the client_id of the application making the access token request
|
||||
Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests
|
||||
Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional).
|
||||
Exp int64 `json:"exp"` // the expiration time of the assertion (seconds since Unix epoch)
|
||||
Iat int64 `json:"iat"` // the time the assertion was issued (seconds since Unix epoch)
|
||||
Typ string `json:"typ,omitempty"` // token type (Optional).
|
||||
|
||||
// Email for which the application is requesting delegated access (Optional).
|
||||
Sub string `json:"sub,omitempty"` |
||||
|
||||
// The old name of Sub. Client keeps setting Prn to be
|
||||
// complaint with legacy OAuth 2.0 providers. (Optional)
|
||||
Prn string `json:"prn,omitempty"` |
||||
|
||||
// See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3
|
||||
// This array is marshalled using custom code (see (c *ClaimSet) encode()).
|
||||
PrivateClaims map[string]interface{} `json:"-"` |
||||
} |
||||
|
||||
func (c *ClaimSet) encode() (string, error) { |
||||
// Reverting time back for machines whose time is not perfectly in sync.
|
||||
// If client machine's time is in the future according
|
||||
// to Google servers, an access token will not be issued.
|
||||
now := time.Now().Add(-10 * time.Second) |
||||
if c.Iat == 0 { |
||||
c.Iat = now.Unix() |
||||
} |
||||
if c.Exp == 0 { |
||||
c.Exp = now.Add(time.Hour).Unix() |
||||
} |
||||
if c.Exp < c.Iat { |
||||
return "", fmt.Errorf("jws: invalid Exp = %v; must be later than Iat = %v", c.Exp, c.Iat) |
||||
} |
||||
|
||||
b, err := json.Marshal(c) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
if len(c.PrivateClaims) == 0 { |
||||
return base64.RawURLEncoding.EncodeToString(b), nil |
||||
} |
||||
|
||||
// Marshal private claim set and then append it to b.
|
||||
prv, err := json.Marshal(c.PrivateClaims) |
||||
if err != nil { |
||||
return "", fmt.Errorf("jws: invalid map of private claims %v", c.PrivateClaims) |
||||
} |
||||
|
||||
// Concatenate public and private claim JSON objects.
|
||||
if !bytes.HasSuffix(b, []byte{'}'}) { |
||||
return "", fmt.Errorf("jws: invalid JSON %s", b) |
||||
} |
||||
if !bytes.HasPrefix(prv, []byte{'{'}) { |
||||
return "", fmt.Errorf("jws: invalid JSON %s", prv) |
||||
} |
||||
b[len(b)-1] = ',' // Replace closing curly brace with a comma.
|
||||
b = append(b, prv[1:]...) // Append private claims.
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil |
||||
} |
||||
|
||||
// Header represents the header for the signed JWS payloads.
|
||||
type Header struct { |
||||
// The algorithm used for signature.
|
||||
Algorithm string `json:"alg"` |
||||
|
||||
// Represents the token type.
|
||||
Typ string `json:"typ"` |
||||
|
||||
// The optional hint of which key is being used.
|
||||
KeyID string `json:"kid,omitempty"` |
||||
} |
||||
|
||||
func (h *Header) encode() (string, error) { |
||||
b, err := json.Marshal(h) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return base64.RawURLEncoding.EncodeToString(b), nil |
||||
} |
||||
|
||||
// Decode decodes a claim set from a JWS payload.
|
||||
func Decode(payload string) (*ClaimSet, error) { |
||||
// decode returned id token to get expiry
|
||||
s := strings.Split(payload, ".") |
||||
if len(s) < 2 { |
||||
// TODO(jbd): Provide more context about the error.
|
||||
return nil, errors.New("jws: invalid token received") |
||||
} |
||||
decoded, err := base64.RawURLEncoding.DecodeString(s[1]) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
c := &ClaimSet{} |
||||
err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c) |
||||
return c, err |
||||
} |
||||
|
||||
// Signer returns a signature for the given data.
|
||||
type Signer func(data []byte) (sig []byte, err error) |
||||
|
||||
// EncodeWithSigner encodes a header and claim set with the provided signer.
|
||||
func EncodeWithSigner(header *Header, c *ClaimSet, sg Signer) (string, error) { |
||||
head, err := header.encode() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
cs, err := c.encode() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
ss := fmt.Sprintf("%s.%s", head, cs) |
||||
sig, err := sg([]byte(ss)) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(sig)), nil |
||||
} |
||||
|
||||
// Encode encodes a signed JWS with provided header and claim set.
|
||||
// This invokes EncodeWithSigner using crypto/rsa.SignPKCS1v15 with the given RSA private key.
|
||||
func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error) { |
||||
sg := func(data []byte) (sig []byte, err error) { |
||||
h := sha256.New() |
||||
h.Write(data) |
||||
return rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil)) |
||||
} |
||||
return EncodeWithSigner(header, c, sg) |
||||
} |
||||
|
||||
// Verify tests whether the provided JWT token's signature was produced by the private key
|
||||
// associated with the supplied public key.
|
||||
func Verify(token string, key *rsa.PublicKey) error { |
||||
parts := strings.Split(token, ".") |
||||
if len(parts) != 3 { |
||||
return errors.New("jws: invalid token received, token must have 3 parts") |
||||
} |
||||
|
||||
signedContent := parts[0] + "." + parts[1] |
||||
signatureString, err := base64.RawURLEncoding.DecodeString(parts[2]) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
h := sha256.New() |
||||
h.Write([]byte(signedContent)) |
||||
return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), []byte(signatureString)) |
||||
} |
||||
@ -0,0 +1,159 @@ |
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package jwt implements the OAuth 2.0 JSON Web Token flow, commonly
|
||||
// known as "two-legged OAuth 2.0".
|
||||
//
|
||||
// See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12
|
||||
package jwt |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
"time" |
||||
|
||||
"golang.org/x/net/context" |
||||
"golang.org/x/oauth2" |
||||
"golang.org/x/oauth2/internal" |
||||
"golang.org/x/oauth2/jws" |
||||
) |
||||
|
||||
var ( |
||||
defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" |
||||
defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"} |
||||
) |
||||
|
||||
// Config is the configuration for using JWT to fetch tokens,
|
||||
// commonly known as "two-legged OAuth 2.0".
|
||||
type Config struct { |
||||
// Email is the OAuth client identifier used when communicating with
|
||||
// the configured OAuth provider.
|
||||
Email string |
||||
|
||||
// PrivateKey contains the contents of an RSA private key or the
|
||||
// contents of a PEM file that contains a private key. The provided
|
||||
// private key is used to sign JWT payloads.
|
||||
// PEM containers with a passphrase are not supported.
|
||||
// Use the following command to convert a PKCS 12 file into a PEM.
|
||||
//
|
||||
// $ openssl pkcs12 -in key.p12 -out key.pem -nodes
|
||||
//
|
||||
PrivateKey []byte |
||||
|
||||
// PrivateKeyID contains an optional hint indicating which key is being
|
||||
// used.
|
||||
PrivateKeyID string |
||||
|
||||
// Subject is the optional user to impersonate.
|
||||
Subject string |
||||
|
||||
// Scopes optionally specifies a list of requested permission scopes.
|
||||
Scopes []string |
||||
|
||||
// TokenURL is the endpoint required to complete the 2-legged JWT flow.
|
||||
TokenURL string |
||||
|
||||
// Expires optionally specifies how long the token is valid for.
|
||||
Expires time.Duration |
||||
} |
||||
|
||||
// TokenSource returns a JWT TokenSource using the configuration
|
||||
// in c and the HTTP client from the provided context.
|
||||
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { |
||||
return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c}) |
||||
} |
||||
|
||||
// Client returns an HTTP client wrapping the context's
|
||||
// HTTP transport and adding Authorization headers with tokens
|
||||
// obtained from c.
|
||||
//
|
||||
// The returned client and its Transport should not be modified.
|
||||
func (c *Config) Client(ctx context.Context) *http.Client { |
||||
return oauth2.NewClient(ctx, c.TokenSource(ctx)) |
||||
} |
||||
|
||||
// jwtSource is a source that always does a signed JWT request for a token.
|
||||
// It should typically be wrapped with a reuseTokenSource.
|
||||
type jwtSource struct { |
||||
ctx context.Context |
||||
conf *Config |
||||
} |
||||
|
||||
func (js jwtSource) Token() (*oauth2.Token, error) { |
||||
pk, err := internal.ParseKey(js.conf.PrivateKey) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
hc := oauth2.NewClient(js.ctx, nil) |
||||
claimSet := &jws.ClaimSet{ |
||||
Iss: js.conf.Email, |
||||
Scope: strings.Join(js.conf.Scopes, " "), |
||||
Aud: js.conf.TokenURL, |
||||
} |
||||
if subject := js.conf.Subject; subject != "" { |
||||
claimSet.Sub = subject |
||||
// prn is the old name of sub. Keep setting it
|
||||
// to be compatible with legacy OAuth 2.0 providers.
|
||||
claimSet.Prn = subject |
||||
} |
||||
if t := js.conf.Expires; t > 0 { |
||||
claimSet.Exp = time.Now().Add(t).Unix() |
||||
} |
||||
h := *defaultHeader |
||||
h.KeyID = js.conf.PrivateKeyID |
||||
payload, err := jws.Encode(&h, claimSet, pk) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
v := url.Values{} |
||||
v.Set("grant_type", defaultGrantType) |
||||
v.Set("assertion", payload) |
||||
resp, err := hc.PostForm(js.conf.TokenURL, v) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) |
||||
} |
||||
defer resp.Body.Close() |
||||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) |
||||
} |
||||
if c := resp.StatusCode; c < 200 || c > 299 { |
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", resp.Status, body) |
||||
} |
||||
// tokenRes is the JSON response body.
|
||||
var tokenRes struct { |
||||
AccessToken string `json:"access_token"` |
||||
TokenType string `json:"token_type"` |
||||
IDToken string `json:"id_token"` |
||||
ExpiresIn int64 `json:"expires_in"` // relative seconds from now
|
||||
} |
||||
if err := json.Unmarshal(body, &tokenRes); err != nil { |
||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) |
||||
} |
||||
token := &oauth2.Token{ |
||||
AccessToken: tokenRes.AccessToken, |
||||
TokenType: tokenRes.TokenType, |
||||
} |
||||
raw := make(map[string]interface{}) |
||||
json.Unmarshal(body, &raw) // no error checks for optional fields
|
||||
token = token.WithExtra(raw) |
||||
|
||||
if secs := tokenRes.ExpiresIn; secs > 0 { |
||||
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second) |
||||
} |
||||
if v := tokenRes.IDToken; v != "" { |
||||
// decode returned id token to get expiry
|
||||
claimSet, err := jws.Decode(v) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err) |
||||
} |
||||
token.Expiry = time.Unix(claimSet.Exp, 0) |
||||
} |
||||
return token, nil |
||||
} |
||||
Loading…
Reference in new issue