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 }