github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/builtin/providers/google/data_source_storage_object_signed_url.go (about)

     1  package google
     2  
     3  import (
     4  	"bytes"
     5  	"crypto"
     6  	"crypto/rand"
     7  	"crypto/rsa"
     8  	"crypto/sha256"
     9  	"crypto/x509"
    10  	"encoding/base64"
    11  	"encoding/pem"
    12  	"errors"
    13  	"fmt"
    14  	"log"
    15  	"net/url"
    16  	"os"
    17  	"strconv"
    18  	"strings"
    19  	"time"
    20  
    21  	"sort"
    22  
    23  	"github.com/hashicorp/errwrap"
    24  	"github.com/hashicorp/terraform/helper/pathorcontents"
    25  	"github.com/hashicorp/terraform/helper/schema"
    26  	"golang.org/x/oauth2/google"
    27  	"golang.org/x/oauth2/jwt"
    28  )
    29  
    30  const gcsBaseUrl = "https://storage.googleapis.com"
    31  const googleCredentialsEnvVar = "GOOGLE_APPLICATION_CREDENTIALS"
    32  
    33  func dataSourceGoogleSignedUrl() *schema.Resource {
    34  	return &schema.Resource{
    35  		Read: dataSourceGoogleSignedUrlRead,
    36  
    37  		Schema: map[string]*schema.Schema{
    38  			"bucket": &schema.Schema{
    39  				Type:     schema.TypeString,
    40  				Required: true,
    41  			},
    42  			"content_md5": &schema.Schema{
    43  				Type:     schema.TypeString,
    44  				Optional: true,
    45  				Default:  "",
    46  			},
    47  			"content_type": &schema.Schema{
    48  				Type:     schema.TypeString,
    49  				Optional: true,
    50  				Default:  "",
    51  			},
    52  			"credentials": &schema.Schema{
    53  				Type:     schema.TypeString,
    54  				Optional: true,
    55  			},
    56  			"duration": &schema.Schema{
    57  				Type:     schema.TypeString,
    58  				Optional: true,
    59  				Default:  "1h",
    60  			},
    61  			"extension_headers": &schema.Schema{
    62  				Type:         schema.TypeMap,
    63  				Optional:     true,
    64  				Elem:         schema.TypeString,
    65  				ValidateFunc: validateExtensionHeaders,
    66  			},
    67  			"http_method": &schema.Schema{
    68  				Type:         schema.TypeString,
    69  				Optional:     true,
    70  				Default:      "GET",
    71  				ValidateFunc: validateHttpMethod,
    72  			},
    73  			"path": &schema.Schema{
    74  				Type:     schema.TypeString,
    75  				Required: true,
    76  			},
    77  			"signed_url": &schema.Schema{
    78  				Type:     schema.TypeString,
    79  				Computed: true,
    80  			},
    81  		},
    82  	}
    83  }
    84  
    85  func validateExtensionHeaders(v interface{}, k string) (ws []string, errors []error) {
    86  	hdrMap := v.(map[string]interface{})
    87  	for k, _ := range hdrMap {
    88  		if !strings.HasPrefix(strings.ToLower(k), "x-goog-") {
    89  			errors = append(errors, fmt.Errorf(
    90  				"extension_header (%s) not valid, header name must begin with 'x-goog-'", k))
    91  		}
    92  	}
    93  	return
    94  }
    95  
    96  func validateHttpMethod(v interface{}, k string) (ws []string, errs []error) {
    97  	value := v.(string)
    98  	value = strings.ToUpper(value)
    99  	if value != "GET" && value != "HEAD" && value != "PUT" && value != "DELETE" {
   100  		errs = append(errs, errors.New("http_method must be one of [GET|HEAD|PUT|DELETE]"))
   101  	}
   102  	return
   103  }
   104  
   105  func dataSourceGoogleSignedUrlRead(d *schema.ResourceData, meta interface{}) error {
   106  	config := meta.(*Config)
   107  
   108  	// Build UrlData object from data source attributes
   109  	urlData := &UrlData{}
   110  
   111  	// HTTP Method
   112  	if method, ok := d.GetOk("http_method"); ok {
   113  		urlData.HttpMethod = method.(string)
   114  	}
   115  
   116  	// convert duration to an expiration datetime (unix time in seconds)
   117  	durationString := "1h"
   118  	if v, ok := d.GetOk("duration"); ok {
   119  		durationString = v.(string)
   120  	}
   121  	duration, err := time.ParseDuration(durationString)
   122  	if err != nil {
   123  		return errwrap.Wrapf("could not parse duration: {{err}}", err)
   124  	}
   125  	expires := time.Now().Unix() + int64(duration.Seconds())
   126  	urlData.Expires = int(expires)
   127  
   128  	// content_md5 is optional
   129  	if v, ok := d.GetOk("content_md5"); ok {
   130  		urlData.ContentMd5 = v.(string)
   131  	}
   132  
   133  	// content_type is optional
   134  	if v, ok := d.GetOk("content_type"); ok {
   135  		urlData.ContentType = v.(string)
   136  	}
   137  
   138  	// extension_headers (x-goog-* HTTP headers) are optional
   139  	if v, ok := d.GetOk("extension_headers"); ok {
   140  		hdrMap := v.(map[string]interface{})
   141  
   142  		if len(hdrMap) > 0 {
   143  			urlData.HttpHeaders = make(map[string]string, len(hdrMap))
   144  			for k, v := range hdrMap {
   145  				urlData.HttpHeaders[k] = v.(string)
   146  			}
   147  		}
   148  	}
   149  
   150  	urlData.Path = fmt.Sprintf("/%s/%s", d.Get("bucket").(string), d.Get("path").(string))
   151  
   152  	// Load JWT Config from Google Credentials
   153  	jwtConfig, err := loadJwtConfig(d, config)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	urlData.JwtConfig = jwtConfig
   158  
   159  	// Construct URL
   160  	signedUrl, err := urlData.SignedUrl()
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	// Success
   166  	d.Set("signed_url", signedUrl)
   167  
   168  	encodedSig, err := urlData.EncodedSignature()
   169  	if err != nil {
   170  		return err
   171  	}
   172  	d.SetId(encodedSig)
   173  
   174  	return nil
   175  }
   176  
   177  // loadJwtConfig looks for credentials json in the following places,
   178  // in order of preference:
   179  //   1. `credentials` attribute of the datasource
   180  //   2. `credentials` attribute in the provider definition.
   181  //   3. A JSON file whose path is specified by the
   182  //      GOOGLE_APPLICATION_CREDENTIALS environment variable.
   183  func loadJwtConfig(d *schema.ResourceData, meta interface{}) (*jwt.Config, error) {
   184  	config := meta.(*Config)
   185  
   186  	credentials := ""
   187  	if v, ok := d.GetOk("credentials"); ok {
   188  		log.Println("[DEBUG] using data source credentials to sign URL")
   189  		credentials = v.(string)
   190  
   191  	} else if config.Credentials != "" {
   192  		log.Println("[DEBUG] using provider credentials to sign URL")
   193  		credentials = config.Credentials
   194  
   195  	} else if filename := os.Getenv(googleCredentialsEnvVar); filename != "" {
   196  		log.Println("[DEBUG] using env GOOGLE_APPLICATION_CREDENTIALS credentials to sign URL")
   197  		credentials = filename
   198  
   199  	}
   200  
   201  	if strings.TrimSpace(credentials) != "" {
   202  		contents, _, err := pathorcontents.Read(credentials)
   203  		if err != nil {
   204  			return nil, errwrap.Wrapf("Error loading credentials: {{err}}", err)
   205  		}
   206  
   207  		cfg, err := google.JWTConfigFromJSON([]byte(contents), "")
   208  		if err != nil {
   209  			return nil, errwrap.Wrapf("Error parsing credentials: {{err}}", err)
   210  		}
   211  		return cfg, nil
   212  	}
   213  
   214  	return nil, errors.New("Credentials not found in datasource, provider configuration or GOOGLE_APPLICATION_CREDENTIALS environment variable.")
   215  }
   216  
   217  // parsePrivateKey converts the binary contents of a private key file
   218  // to an *rsa.PrivateKey. It detects whether the private key is in a
   219  // PEM container or not. If so, it extracts the the private key
   220  // from PEM container before conversion. It only supports PEM
   221  // containers with no passphrase.
   222  // copied from golang.org/x/oauth2/internal
   223  func parsePrivateKey(key []byte) (*rsa.PrivateKey, error) {
   224  	block, _ := pem.Decode(key)
   225  	if block != nil {
   226  		key = block.Bytes
   227  	}
   228  	parsedKey, err := x509.ParsePKCS8PrivateKey(key)
   229  	if err != nil {
   230  		parsedKey, err = x509.ParsePKCS1PrivateKey(key)
   231  		if err != nil {
   232  			return nil, errwrap.Wrapf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: {{err}}", err)
   233  		}
   234  	}
   235  	parsed, ok := parsedKey.(*rsa.PrivateKey)
   236  	if !ok {
   237  		return nil, errors.New("private key is invalid")
   238  	}
   239  	return parsed, nil
   240  }
   241  
   242  // UrlData stores the values required to create a Signed Url
   243  type UrlData struct {
   244  	JwtConfig   *jwt.Config
   245  	ContentMd5  string
   246  	ContentType string
   247  	HttpMethod  string
   248  	Expires     int
   249  	HttpHeaders map[string]string
   250  	Path        string
   251  }
   252  
   253  // SigningString creates a string representation of the UrlData in a form ready for signing:
   254  // see https://cloud.google.com/storage/docs/access-control/create-signed-urls-program
   255  // Example output:
   256  // -------------------
   257  // GET
   258  //
   259  //
   260  // 1388534400
   261  // bucket/objectname
   262  // -------------------
   263  func (u *UrlData) SigningString() []byte {
   264  	var buf bytes.Buffer
   265  
   266  	// HTTP Verb
   267  	buf.WriteString(u.HttpMethod)
   268  	buf.WriteString("\n")
   269  
   270  	// Content MD5 (optional, always add new line)
   271  	buf.WriteString(u.ContentMd5)
   272  	buf.WriteString("\n")
   273  
   274  	// Content Type (optional, always add new line)
   275  	buf.WriteString(u.ContentType)
   276  	buf.WriteString("\n")
   277  
   278  	// Expiration
   279  	buf.WriteString(strconv.Itoa(u.Expires))
   280  	buf.WriteString("\n")
   281  
   282  	// Extra HTTP headers (optional)
   283  	// Must be sorted in lexigraphical order
   284  	var keys []string
   285  	for k := range u.HttpHeaders {
   286  		keys = append(keys, strings.ToLower(k))
   287  	}
   288  	sort.Strings(keys)
   289  	// Write sorted headers to signing string buffer
   290  	for _, k := range keys {
   291  		buf.WriteString(fmt.Sprintf("%s:%s\n", k, u.HttpHeaders[k]))
   292  	}
   293  
   294  	// Storate Object path (includes bucketname)
   295  	buf.WriteString(u.Path)
   296  
   297  	return buf.Bytes()
   298  }
   299  
   300  func (u *UrlData) Signature() ([]byte, error) {
   301  	// Sign url data
   302  	signature, err := SignString(u.SigningString(), u.JwtConfig)
   303  	if err != nil {
   304  		return nil, err
   305  
   306  	}
   307  
   308  	return signature, nil
   309  }
   310  
   311  // EncodedSignature returns the Signature() after base64 encoding and url escaping
   312  func (u *UrlData) EncodedSignature() (string, error) {
   313  	signature, err := u.Signature()
   314  	if err != nil {
   315  		return "", err
   316  	}
   317  
   318  	// base64 encode signature
   319  	encoded := base64.StdEncoding.EncodeToString(signature)
   320  	// encoded signature may include /, = characters that need escaping
   321  	encoded = url.QueryEscape(encoded)
   322  
   323  	return encoded, nil
   324  }
   325  
   326  // SignedUrl constructs the final signed URL a client can use to retrieve storage object
   327  func (u *UrlData) SignedUrl() (string, error) {
   328  
   329  	encodedSig, err := u.EncodedSignature()
   330  	if err != nil {
   331  		return "", err
   332  	}
   333  
   334  	// build url
   335  	// https://cloud.google.com/storage/docs/access-control/create-signed-urls-program
   336  	var urlBuffer bytes.Buffer
   337  	urlBuffer.WriteString(gcsBaseUrl)
   338  	urlBuffer.WriteString(u.Path)
   339  	urlBuffer.WriteString("?GoogleAccessId=")
   340  	urlBuffer.WriteString(u.JwtConfig.Email)
   341  	urlBuffer.WriteString("&Expires=")
   342  	urlBuffer.WriteString(strconv.Itoa(u.Expires))
   343  	urlBuffer.WriteString("&Signature=")
   344  	urlBuffer.WriteString(encodedSig)
   345  
   346  	return urlBuffer.String(), nil
   347  }
   348  
   349  // SignString calculates the SHA256 signature of the input string
   350  func SignString(toSign []byte, cfg *jwt.Config) ([]byte, error) {
   351  	// Parse private key
   352  	pk, err := parsePrivateKey(cfg.PrivateKey)
   353  	if err != nil {
   354  		return nil, errwrap.Wrapf("failed to sign string, could not parse key: {{err}}", err)
   355  	}
   356  
   357  	// Hash string
   358  	hasher := sha256.New()
   359  	hasher.Write(toSign)
   360  
   361  	// Sign string
   362  	signed, err := rsa.SignPKCS1v15(rand.Reader, pk, crypto.SHA256, hasher.Sum(nil))
   363  	if err != nil {
   364  		return nil, errwrap.Wrapf("failed to sign string, an error occurred: {{err}}", err)
   365  	}
   366  
   367  	return signed, nil
   368  }