google-cloud-storage 1.25.1 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab43474a1a6a25a439d96a67b8664caff72861d3d121c927d5c11623ae570cab
4
- data.tar.gz: f2e6378f9708fc079f2ce3539947e5587eb57263b2d96c8877c93d1b07b30abd
3
+ metadata.gz: 6a69b7d03384d604c8b96548b952b6894af09ee7cef01845fe92b998ea77bcc8
4
+ data.tar.gz: 9fd54043ddab11b2284c73b17e780877b795a59593943deef43b9d14fbb4da1e
5
5
  SHA512:
6
- metadata.gz: d40dfe073d42be6d3098a5c5e8779c4c52a7407b77f159e435e8c76ffe1cc59fc037591bcbc1b00ebaea9763d6085c7a9e639549eaacd89ea1444c1cd81a75e3
7
- data.tar.gz: 16c8f515149d0794c40a474fb43d6c1859ff427b1ab5c75c6f4610959c49e3ed1a2c8d3a0060be58ded02f34867437fcaf81c1e2a8e00a8320af2fff470e5b34
6
+ metadata.gz: 36d6b3d59ab2655020f97e7d15a33a35b48a541632e03a6a8aa01f20289c821b9582cf21fdea2799cad8111c8208bd866154246a8ec69fb33102fd78853bc49f
7
+ data.tar.gz: 8d59996b1108a4c44e074e9762e48ad4a1f93c8dfd07efa11b77472a3b2285be52ce1486afa76b5301de683cf918c23d41d1d0dd877cd4bbaf0ab3d1cc91e405
@@ -1,5 +1,22 @@
1
1
  # Release History
2
2
 
3
+ ### 1.26.0 / 2020-04-06
4
+
5
+ #### Features
6
+
7
+ * Update V4 Signature support in Project#signed_url, Bucket#signed_url and File#signed_url
8
+ * Add scheme, virtual_hosted_style and bucket_bound_hostname to #signed_url methods
9
+ * Add support for V4 query param encoding and ordering
10
+ * Convert tabs in V4 to single whitespace character
11
+ * Set payload in V4 to X-Goog-Content-SHA256 if present
12
+ * Fix method param default value GET for #signed_url
13
+ * Add support for V4 Signature POST Policies
14
+ * Add Bucket#generate_signed_post_policy_v4
15
+
16
+ #### Bug Fixes
17
+
18
+ * Address keyword argument warnings in Ruby 2.7 and later
19
+
3
20
  ### 1.25.1 / 2020-01-06
4
21
 
5
22
  #### Documentation
@@ -1254,22 +1254,27 @@ module Google
1254
1254
  storage_class: nil, encryption_key: nil, kms_key: nil,
1255
1255
  temporary_hold: nil, event_based_hold: nil
1256
1256
  ensure_service!
1257
- options = { acl: File::Acl.predefined_rule_for(acl), md5: md5,
1258
- cache_control: cache_control, content_type: content_type,
1259
- content_disposition: content_disposition, crc32c: crc32c,
1260
- content_encoding: content_encoding, metadata: metadata,
1261
- content_language: content_language, key: encryption_key,
1262
- kms_key: kms_key,
1263
- storage_class: storage_class_for(storage_class),
1264
- temporary_hold: temporary_hold,
1265
- event_based_hold: event_based_hold,
1266
- user_project: user_project }
1267
1257
  ensure_io_or_file_exists! file
1268
1258
  path ||= file.path if file.respond_to? :path
1269
1259
  path ||= file if file.is_a? String
1270
1260
  raise ArgumentError, "must provide path" if path.nil?
1271
1261
 
1272
- gapi = service.insert_file name, file, path, options
1262
+
1263
+ gapi = service.insert_file name, file, path, acl: File::Acl.predefined_rule_for(acl),
1264
+ md5: md5,
1265
+ cache_control: cache_control,
1266
+ content_type: content_type,
1267
+ content_disposition: content_disposition,
1268
+ crc32c: crc32c,
1269
+ content_encoding: content_encoding,
1270
+ metadata: metadata,
1271
+ content_language: content_language,
1272
+ key: encryption_key,
1273
+ kms_key: kms_key,
1274
+ storage_class: storage_class_for(storage_class),
1275
+ temporary_hold: temporary_hold,
1276
+ event_based_hold: event_based_hold,
1277
+ user_project: user_project
1273
1278
  File.from_gapi gapi, service, user_project: user_project
1274
1279
  end
1275
1280
  alias upload_file create_file
@@ -1368,9 +1373,6 @@ module Google
1368
1373
  raise ArgumentError, "must provide at least two source files"
1369
1374
  end
1370
1375
 
1371
- options = { acl: File::Acl.predefined_rule_for(acl),
1372
- key: encryption_key,
1373
- user_project: user_project }
1374
1376
  destination_gapi = nil
1375
1377
  if block_given?
1376
1378
  destination_gapi = API::Object.new
@@ -1378,8 +1380,11 @@ module Google
1378
1380
  yield updater
1379
1381
  updater.check_for_changed_metadata!
1380
1382
  end
1381
- gapi = service.compose_file name, sources, destination,
1382
- destination_gapi, options
1383
+
1384
+ acl_rule = File::Acl.predefined_rule_for acl
1385
+ gapi = service.compose_file name, sources, destination, destination_gapi, acl: acl_rule,
1386
+ key: encryption_key,
1387
+ user_project: user_project
1383
1388
  File.from_gapi gapi, service, user_project: user_project
1384
1389
  end
1385
1390
  alias compose_file compose
@@ -1440,6 +1445,19 @@ module Google
1440
1445
  # using the URL, but only when the file resource is missing the
1441
1446
  # corresponding values. (These values can be permanently set using
1442
1447
  # {File#content_disposition=} and {File#content_type=}.)
1448
+ # @param [String] scheme The URL scheme. The default value is `HTTPS`.
1449
+ # @param [Boolean] virtual_hosted_style Whether to use a virtual hosted-style
1450
+ # hostname, which adds the bucket into the host portion of the URI rather
1451
+ # than the path, e.g. `https://mybucket.storage.googleapis.com/...`.
1452
+ # For V4 signing, this also sets the `host` header in the canonicalized
1453
+ # extension headers to the virtual hosted-style host, unless that header is
1454
+ # supplied via the `headers` param. The default value of `false` uses the
1455
+ # form of `https://storage.googleapis.com/mybucket`.
1456
+ # @param [String] bucket_bound_hostname Use a bucket-bound hostname, which
1457
+ # replaces the `storage.googleapis.com` host with the name of a `CNAME`
1458
+ # bucket, e.g. a bucket named `gcs-subdomain.my.domain.tld`, or a Google
1459
+ # Cloud Load Balancer which routes to a bucket you own, e.g.
1460
+ # `my-load-balancer-domain.tld`.
1443
1461
  # @param [Symbol, String] version The version of the signed credential
1444
1462
  # to create. Must be one of `:v2` or `:v4`. The default value is
1445
1463
  # `:v2`.
@@ -1510,28 +1528,49 @@ module Google
1510
1528
  # bucket = storage.bucket "my-todo-app"
1511
1529
  # list_files_url = bucket.signed_url version: :v4
1512
1530
  #
1513
- def signed_url path = nil, method: nil, expires: nil, content_type: nil,
1514
- content_md5: nil, headers: nil, issuer: nil,
1515
- client_email: nil, signing_key: nil, private_key: nil,
1516
- query: nil, version: nil
1531
+ def signed_url path = nil,
1532
+ method: "GET",
1533
+ expires: nil,
1534
+ content_type: nil,
1535
+ content_md5: nil,
1536
+ headers: nil,
1537
+ issuer: nil,
1538
+ client_email: nil,
1539
+ signing_key: nil,
1540
+ private_key: nil,
1541
+ query: nil,
1542
+ scheme: "HTTPS",
1543
+ virtual_hosted_style: nil,
1544
+ bucket_bound_hostname: nil,
1545
+ version: nil
1517
1546
  ensure_service!
1518
1547
  version ||= :v2
1519
1548
  case version.to_sym
1520
1549
  when :v2
1521
1550
  signer = File::SignerV2.from_bucket self, path
1522
- signer.signed_url method: method, expires: expires,
1523
- headers: headers, content_type: content_type,
1524
- content_md5: content_md5, issuer: issuer,
1551
+ signer.signed_url method: method,
1552
+ expires: expires,
1553
+ headers: headers,
1554
+ content_type: content_type,
1555
+ content_md5: content_md5,
1556
+ issuer: issuer,
1525
1557
  client_email: client_email,
1526
1558
  signing_key: signing_key,
1527
- private_key: private_key, query: query
1559
+ private_key: private_key,
1560
+ query: query
1528
1561
  when :v4
1529
1562
  signer = File::SignerV4.from_bucket self, path
1530
- signer.signed_url method: method, expires: expires,
1531
- headers: headers, issuer: issuer,
1563
+ signer.signed_url method: method,
1564
+ expires: expires,
1565
+ headers: headers,
1566
+ issuer: issuer,
1532
1567
  client_email: client_email,
1533
1568
  signing_key: signing_key,
1534
- private_key: private_key, query: query
1569
+ private_key: private_key,
1570
+ query: query,
1571
+ scheme: scheme,
1572
+ virtual_hosted_style: virtual_hosted_style,
1573
+ bucket_bound_hostname: bucket_bound_hostname
1535
1574
  else
1536
1575
  raise ArgumentError, "version '#{version}' not supported"
1537
1576
  end
@@ -1558,12 +1597,13 @@ module Google
1558
1597
  #
1559
1598
  # @param [String] path Path to the file in Google Cloud Storage.
1560
1599
  # @param [Hash] policy The security policy that describes what
1561
- # can and cannot be uploaded in the form. When provided,
1562
- # the PostObject fields will include a Signature based on the JSON
1563
- # representation of this Hash and the same policy in Base64 format.
1600
+ # can and cannot be uploaded in the form. When provided, the PostObject
1601
+ # fields will include a signature based on the JSON representation of
1602
+ # this hash and the same policy in Base64 format.
1603
+ #
1564
1604
  # If you do not provide a security policy, requests are considered
1565
1605
  # to be anonymous and will only work with buckets that have granted
1566
- # WRITE or FULL_CONTROL permission to anonymous users.
1606
+ # `WRITE` or `FULL_CONTROL` permission to anonymous users.
1567
1607
  # See [Policy Document](https://cloud.google.com/storage/docs/xml-api/post-object#policydocument)
1568
1608
  # for more information.
1569
1609
  # @param [String] issuer Service Account's Client Email.
@@ -1633,17 +1673,110 @@ module Google
1633
1673
  # post.fields[:signature] #=> "ABC...XYZ="
1634
1674
  # post.fields[:policy] #=> "ABC...XYZ="
1635
1675
  #
1636
- def post_object path, policy: nil, issuer: nil,
1637
- client_email: nil, signing_key: nil,
1676
+ def post_object path,
1677
+ policy: nil,
1678
+ issuer: nil,
1679
+ client_email: nil,
1680
+ signing_key: nil,
1638
1681
  private_key: nil
1639
1682
  ensure_service!
1640
-
1641
1683
  signer = File::SignerV2.from_bucket self, path
1642
- signer.post_object issuer: issuer, client_email: client_email,
1643
- signing_key: signing_key, private_key: private_key,
1684
+ signer.post_object issuer: issuer,
1685
+ client_email: client_email,
1686
+ signing_key: signing_key,
1687
+ private_key: private_key,
1644
1688
  policy: policy
1645
1689
  end
1646
1690
 
1691
+ ##
1692
+ # Generate a PostObject that includes the fields and url to
1693
+ # upload objects via html forms.
1694
+ #
1695
+ # Generating a PostObject requires service account credentials,
1696
+ # either by connecting with a service account when calling
1697
+ # {Google::Cloud.storage}, or by passing in the service account
1698
+ # `issuer` and `signing_key` values. Although the private key can
1699
+ # be passed as a string for convenience, creating and storing
1700
+ # an instance of `OpenSSL::PKey::RSA` is more efficient
1701
+ # when making multiple calls to `generate_signed_post_policy_v4`.
1702
+ #
1703
+ # A {SignedUrlUnavailable} is raised if the service account credentials
1704
+ # are missing. Service account credentials are acquired by following the
1705
+ # steps in [Service Account Authentication](
1706
+ # https://cloud.google.com/storage/docs/authentication#service_accounts).
1707
+ #
1708
+ # @see https://cloud.google.com/storage/docs/xml-api/post-object
1709
+ #
1710
+ # @param [String] path Path to the file in Google Cloud Storage.
1711
+ # @param [String] issuer Service Account's Client Email.
1712
+ # @param [String] client_email Service Account's Client Email.
1713
+ # @param [OpenSSL::PKey::RSA, String] signing_key Service Account's
1714
+ # Private Key.
1715
+ # @param [OpenSSL::PKey::RSA, String] private_key Service Account's
1716
+ # Private Key.
1717
+ # @param [Integer] expires The number of seconds until the URL expires.
1718
+ # The default is 604800 (7 days).
1719
+ # @param [Hash] fields User-supplied form fields such as `acl`,
1720
+ # `cache-control`, `success_action_status`, and `success_action_redirect`.
1721
+ # @param [Array<Hash|Array>] conditions User-supplied policy conditions.
1722
+ # @param [String] scheme The URL scheme. The default value is `HTTPS`.
1723
+ # @param [Boolean] virtual_hosted_style Whether to use a virtual hosted-style
1724
+ # hostname, which adds the bucket into the host portion of the URI rather
1725
+ # than the path, e.g. `https://mybucket.storage.googleapis.com/...`.
1726
+ # The default value of `false` uses the
1727
+ # form of `https://storage.googleapis.com/mybucket`.
1728
+ # @param [String] bucket_bound_hostname Use a bucket-bound hostname, which
1729
+ # replaces the `storage.googleapis.com` host with the name of a `CNAME`
1730
+ # bucket, e.g. a bucket named `gcs-subdomain.my.domain.tld`, or a Google
1731
+ # Cloud Load Balancer which routes to a bucket you own, e.g.
1732
+ # `my-load-balancer-domain.tld`.
1733
+ #
1734
+ # @return [PostObject] An object containing the URL, fields, and values needed to upload files via html forms.
1735
+ #
1736
+ # @example
1737
+ # require "google/cloud/storage"
1738
+ #
1739
+ # storage = Google::Cloud::Storage.new
1740
+ #
1741
+ # bucket = storage.bucket "my-todo-app"
1742
+ #
1743
+ # conditions = [["starts-with", "$acl","public"]]
1744
+ # post = bucket.generate_signed_post_policy_v4 "avatars/heidi/400x400.png", expires: 10,
1745
+ # conditions: conditions
1746
+ #
1747
+ # post.url #=> "https://storage.googleapis.com/my-todo-app/"
1748
+ # post.fields["key"] #=> "my-todo-app/avatars/heidi/400x400.png"
1749
+ # post.fields["policy"] #=> "ABC...XYZ"
1750
+ # post.fields["x-goog-algorithm"] #=> "GOOG4-RSA-SHA256"
1751
+ # post.fields["x-goog-credential"] #=> "cred@pid.iam.gserviceaccount.com/20200123/auto/storage/goog4_request"
1752
+ # post.fields["x-goog-date"] #=> "20200128T000000Z"
1753
+ # post.fields["x-goog-signature"] #=> "4893a0e...cd82"
1754
+ #
1755
+ def generate_signed_post_policy_v4 path,
1756
+ issuer: nil,
1757
+ client_email: nil,
1758
+ signing_key: nil,
1759
+ private_key: nil,
1760
+ expires: nil,
1761
+ fields: nil,
1762
+ conditions: nil,
1763
+ scheme: "https",
1764
+ virtual_hosted_style: nil,
1765
+ bucket_bound_hostname: nil
1766
+ ensure_service!
1767
+ signer = File::SignerV4.from_bucket self, path
1768
+ signer.post_object issuer: issuer,
1769
+ client_email: client_email,
1770
+ signing_key: signing_key,
1771
+ private_key: private_key,
1772
+ expires: expires,
1773
+ fields: fields,
1774
+ conditions: conditions,
1775
+ scheme: scheme,
1776
+ virtual_hosted_style: virtual_hosted_style,
1777
+ bucket_bound_hostname: bucket_bound_hostname
1778
+ end
1779
+
1647
1780
  ##
1648
1781
  # The {Bucket::Acl} instance used to control access to the bucket.
1649
1782
  #
@@ -2125,11 +2258,12 @@ module Google
2125
2258
  def create_notification topic, custom_attrs: nil, event_types: nil,
2126
2259
  prefix: nil, payload: nil
2127
2260
  ensure_service!
2128
- options = { custom_attrs: custom_attrs, event_types: event_types,
2129
- prefix: prefix, payload: payload,
2130
- user_project: user_project }
2131
2261
 
2132
- gapi = service.insert_notification name, topic, options
2262
+ gapi = service.insert_notification name, topic, custom_attrs: custom_attrs,
2263
+ event_types: event_types,
2264
+ prefix: prefix,
2265
+ payload: payload,
2266
+ user_project: user_project
2133
2267
  Notification.from_gapi name, gapi, service, user_project: user_project
2134
2268
  end
2135
2269
  alias new_notification create_notification
@@ -2215,7 +2349,7 @@ module Google
2215
2349
  patch_args = Hash[attributes.map do |attr|
2216
2350
  [attr, @gapi.send(attr)]
2217
2351
  end]
2218
- patch_gapi = API::Bucket.new patch_args
2352
+ patch_gapi = API::Bucket.new(**patch_args)
2219
2353
  @gapi = service.patch_bucket name, patch_gapi,
2220
2354
  user_project: user_project
2221
2355
  @lazy = nil
@@ -1479,6 +1479,19 @@ module Google
1479
1479
  # using the URL, but only when the file resource is missing the
1480
1480
  # corresponding values. (These values can be permanently set using
1481
1481
  # {#content_disposition=} and {#content_type=}.)
1482
+ # @param [String] scheme The URL scheme. The default value is `HTTPS`.
1483
+ # @param [Boolean] virtual_hosted_style Whether to use a virtual hosted-style
1484
+ # hostname, which adds the bucket into the host portion of the URI rather
1485
+ # than the path, e.g. `https://mybucket.storage.googleapis.com/...`.
1486
+ # For V4 signing, this also sets the `host` header in the canonicalized
1487
+ # extension headers to the virtual hosted-style host, unless that header is
1488
+ # supplied via the `headers` param. The default value of `false` uses the
1489
+ # form of `https://storage.googleapis.com/mybucket`.
1490
+ # @param [String] bucket_bound_hostname Use a bucket-bound hostname, which
1491
+ # replaces the `storage.googleapis.com` host with the name of a `CNAME`
1492
+ # bucket, e.g. a bucket named `gcs-subdomain.my.domain.tld`, or a Google
1493
+ # Cloud Load Balancer which routes to a bucket you own, e.g.
1494
+ # `my-load-balancer-domain.tld`.
1482
1495
  # @param [Symbol, String] version The version of the signed credential
1483
1496
  # to create. Must be one of `:v2` or `:v4`. The default value is
1484
1497
  # `:v2`.
@@ -1503,7 +1516,7 @@ module Google
1503
1516
  # file = bucket.file "avatars/heidi/400x400.png"
1504
1517
  # shared_url = file.signed_url expires: 300, # 5 minutes from now
1505
1518
  # version: :v4
1506
-
1519
+ #
1507
1520
  # @example Using the `issuer` and `signing_key` options:
1508
1521
  # require "google/cloud/storage"
1509
1522
  #
@@ -1543,28 +1556,48 @@ module Google
1543
1556
  # # Send the `x-goog-resumable:start` header and the content type
1544
1557
  # # with the resumable upload POST request.
1545
1558
  #
1546
- def signed_url method: nil, expires: nil, content_type: nil,
1547
- content_md5: nil, headers: nil, issuer: nil,
1548
- client_email: nil, signing_key: nil, private_key: nil,
1549
- query: nil, version: nil
1559
+ def signed_url method: "GET",
1560
+ expires: nil,
1561
+ content_type: nil,
1562
+ content_md5: nil,
1563
+ headers: nil,
1564
+ issuer: nil,
1565
+ client_email: nil,
1566
+ signing_key: nil,
1567
+ private_key: nil,
1568
+ query: nil,
1569
+ scheme: "HTTPS",
1570
+ virtual_hosted_style: nil,
1571
+ bucket_bound_hostname: nil,
1572
+ version: nil
1550
1573
  ensure_service!
1551
1574
  version ||= :v2
1552
1575
  case version.to_sym
1553
1576
  when :v2
1554
1577
  signer = File::SignerV2.from_file self
1555
- signer.signed_url method: method, expires: expires,
1556
- headers: headers, content_type: content_type,
1557
- content_md5: content_md5, issuer: issuer,
1578
+ signer.signed_url method: method,
1579
+ expires: expires,
1580
+ headers: headers,
1581
+ content_type: content_type,
1582
+ content_md5: content_md5,
1583
+ issuer: issuer,
1558
1584
  client_email: client_email,
1559
1585
  signing_key: signing_key,
1560
- private_key: private_key, query: query
1586
+ private_key: private_key,
1587
+ query: query
1561
1588
  when :v4
1562
1589
  signer = File::SignerV4.from_file self
1563
- signer.signed_url method: method, expires: expires,
1564
- headers: headers, issuer: issuer,
1590
+ signer.signed_url method: method,
1591
+ expires: expires,
1592
+ headers: headers,
1593
+ issuer: issuer,
1565
1594
  client_email: client_email,
1566
1595
  signing_key: signing_key,
1567
- private_key: private_key, query: query
1596
+ private_key: private_key,
1597
+ query: query,
1598
+ scheme: scheme,
1599
+ virtual_hosted_style: virtual_hosted_style,
1600
+ bucket_bound_hostname: bucket_bound_hostname
1568
1601
  else
1569
1602
  raise ArgumentError, "version '#{version}' not supported"
1570
1603
  end
@@ -1733,7 +1766,7 @@ module Google
1733
1766
  # Sending nil metadata results in an Apiary runtime error:
1734
1767
  # NoMethodError: undefined method `each' for nil:NilClass
1735
1768
  attr_params.reject! { |k, v| k == :metadata && v.nil? }
1736
- Google::Apis::StorageV1::Object.new attr_params
1769
+ Google::Apis::StorageV1::Object.new(**attr_params)
1737
1770
  end
1738
1771
 
1739
1772
  protected
@@ -1784,12 +1817,12 @@ module Google
1784
1817
  user_project: user_project }.delete_if { |_k, v| v.nil? }
1785
1818
 
1786
1819
  resp = service.rewrite_file \
1787
- bucket, name, new_bucket, new_name, updated_gapi, options
1820
+ bucket, name, new_bucket, new_name, updated_gapi, **options
1788
1821
  until resp.done
1789
1822
  sleep 1
1790
1823
  retry_options = options.merge token: resp.rewrite_token
1791
1824
  resp = service.rewrite_file \
1792
- bucket, name, new_bucket, new_name, updated_gapi, retry_options
1825
+ bucket, name, new_bucket, new_name, updated_gapi, **retry_options
1793
1826
  end
1794
1827
  resp.resource
1795
1828
  end
@@ -77,11 +77,13 @@ module Google
77
77
  def next
78
78
  return nil unless next?
79
79
  ensure_service!
80
- options = {
81
- prefix: @prefix, delimiter: @delimiter, token: @token, max: @max,
82
- versions: @versions, user_project: @user_project
83
- }
84
- gapi = @service.list_files @bucket, options
80
+
81
+ gapi = @service.list_files @bucket, prefix: @prefix,
82
+ delimiter: @delimiter,
83
+ token: @token,
84
+ max: @max,
85
+ versions: @versions,
86
+ user_project: @user_project
85
87
  File::List.from_gapi gapi, @service, @bucket, @prefix,
86
88
  @delimiter, @max, @versions,
87
89
  user_project: @user_project
@@ -39,37 +39,83 @@ module Google
39
39
  new bucket.name, file_name, bucket.service
40
40
  end
41
41
 
42
- def signed_url method: "GET", expires: nil, headers: nil,
43
- issuer: nil, client_email: nil, signing_key: nil,
44
- private_key: nil, query: nil
45
- issuer, signer = issuer_and_signer issuer, client_email,
46
- signing_key, private_key
42
+ def post_object issuer: nil,
43
+ client_email: nil,
44
+ signing_key: nil,
45
+ private_key: nil,
46
+ expires: nil,
47
+ fields: nil,
48
+ conditions: nil,
49
+ scheme: "https",
50
+ virtual_hosted_style: nil,
51
+ bucket_bound_hostname: nil
52
+ i = determine_issuer issuer, client_email
53
+ s = determine_signing_key signing_key, private_key
54
+ raise SignedUrlUnavailable unless i && s
55
+
56
+ now = Time.now.utc
57
+ base_fields = required_fields i, now
58
+ post_fields = fields.dup || {}
59
+ post_fields.merge! base_fields
60
+
61
+ p = {}
62
+ p["conditions"] = policy_conditions base_fields, conditions, fields
63
+ expires ||= 60*60*24
64
+ p["expiration"] = (now + expires).strftime "%Y-%m-%dT%H:%M:%SZ"
65
+
66
+
67
+ policy_str = escape_characters p.to_json
68
+
69
+ policy = Base64.strict_encode64(policy_str).force_encoding "utf-8"
70
+ signature = generate_signature s, policy
71
+
72
+ post_fields["x-goog-signature"] = signature
73
+ post_fields["policy"] = policy
74
+ url = post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname
75
+ hostname = "#{url}#{bucket_path path_style?(virtual_hosted_style, bucket_bound_hostname)}"
76
+ Google::Cloud::Storage::PostObject.new hostname, post_fields
77
+ end
78
+
79
+ def signed_url method: "GET",
80
+ expires: nil,
81
+ headers: nil,
82
+ issuer: nil,
83
+ client_email: nil,
84
+ signing_key: nil,
85
+ private_key: nil,
86
+ query: nil,
87
+ scheme: "https",
88
+ virtual_hosted_style: nil,
89
+ bucket_bound_hostname: nil
90
+ raise ArgumentError, "method is required" unless method
91
+ issuer, signer = issuer_and_signer issuer, client_email, signing_key, private_key
47
92
  datetime_now = Time.now.utc
48
93
  goog_date = datetime_now.strftime "%Y%m%dT%H%M%SZ"
49
94
  datestamp = datetime_now.strftime "%Y%m%d"
50
95
  # goog4_request is not checked.
51
96
  scope = "#{datestamp}/auto/storage/goog4_request"
52
97
 
53
- canonical_headers_str, signed_headers_str = \
54
- canonical_and_signed_headers headers
98
+ canonical_headers_str, signed_headers_str = canonical_and_signed_headers \
99
+ headers, virtual_hosted_style, bucket_bound_hostname
55
100
 
56
101
  algorithm = "GOOG4-RSA-SHA256"
57
102
  expires = determine_expires expires
58
- credential = CGI.escape issuer + "/" + scope
59
- canonical_query_str = canonical_query query, algorithm,
60
- credential, goog_date,
61
- expires, signed_headers_str
103
+ credential = issuer + "/" + scope
104
+ canonical_query_str = canonical_query query, algorithm, credential, goog_date, expires, signed_headers_str
62
105
 
63
106
  # From AWS: You don't include a payload hash in the Canonical
64
107
  # Request, because when you create a presigned URL, you don't know
65
108
  # the payload content because the URL is used to upload an arbitrary
66
109
  # payload. Instead, you use a constant string UNSIGNED-PAYLOAD.
110
+ payload = headers&.key?("X-Goog-Content-SHA256") ? headers["X-Goog-Content-SHA256"] : "UNSIGNED-PAYLOAD"
111
+
67
112
  canonical_request = [method,
68
- ext_path,
113
+ file_path(!(virtual_hosted_style || bucket_bound_hostname)),
69
114
  canonical_query_str,
70
115
  canonical_headers_str,
71
116
  signed_headers_str,
72
- "UNSIGNED-PAYLOAD"].join("\n")
117
+ payload].join("\n")
118
+
73
119
  # Construct string to sign
74
120
  req_sha = Digest::SHA256.hexdigest canonical_request
75
121
  string_to_sign = [algorithm, goog_date, scope, req_sha].join "\n"
@@ -78,33 +124,85 @@ module Google
78
124
  signature = signer.call string_to_sign
79
125
 
80
126
  # Construct signed URL
81
- "#{ext_url}?#{canonical_query_str}&X-Goog-Signature=#{signature}"
127
+ hostname = signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname
128
+ "#{hostname}?#{canonical_query_str}&X-Goog-Signature=#{signature}"
129
+ end
130
+
131
+ # methods below are public visibility only for unit testing
132
+ def escape_characters str
133
+ str.split("").map do |s|
134
+ if !s.ascii_only?
135
+ escape_special_unicode s
136
+ else
137
+ case s
138
+ when "\\"
139
+ '\\'
140
+ when "\b"
141
+ '\b'
142
+ when "\f"
143
+ '\f'
144
+ when "\n"
145
+ '\n'
146
+ when "\r"
147
+ '\r'
148
+ when "\t"
149
+ '\t'
150
+ when "\v"
151
+ '\v'
152
+ else
153
+ s
154
+ end
155
+ end
156
+ end.join
157
+ end
158
+
159
+ def escape_special_unicode str
160
+ str.unpack("U*").map { |i| '\u' + i.to_s(16).rjust(4, "0") }.join
82
161
  end
83
162
 
84
163
  protected
85
164
 
165
+ def required_fields issuer, time
166
+ {
167
+ "key" => @file_name,
168
+ "x-goog-date" => time.strftime("%Y%m%dT%H%M%SZ"),
169
+ "x-goog-credential" => "#{issuer}/#{time.strftime '%Y%m%d'}/auto/storage/goog4_request",
170
+ "x-goog-algorithm" => "GOOG4-RSA-SHA256"
171
+ }.freeze
172
+ end
173
+
174
+ def policy_conditions base_fields, user_conditions, user_fields
175
+ # Convert each pair in base_fields hash to a single-entry hash in an array.
176
+ conditions = base_fields.to_a.map { |f| Hash[*f] }
177
+ # Add user-provided conditions to the head of the conditions array.
178
+ conditions.unshift user_conditions if user_conditions && !user_conditions.empty?
179
+ if user_fields
180
+ # Convert each pair in fields hash to a single-entry hash and add it to the head of the conditions array.
181
+ user_fields.to_a.reverse.each { |f| conditions.unshift Hash[*f] }
182
+ end
183
+ conditions.freeze
184
+ end
185
+
186
+ def signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname
187
+ url = ext_url scheme, virtual_hosted_style, bucket_bound_hostname
188
+ "#{url}#{file_path path_style?(virtual_hosted_style, bucket_bound_hostname)}"
189
+ end
190
+
86
191
  def determine_issuer issuer, client_email
87
192
  # Parse the Service Account and get client id and private key
88
193
  issuer = issuer || client_email || @service.credentials.issuer
89
- unless issuer
90
- raise SignedUrlUnavailable, "issuer (client_email) missing"
91
- end
194
+ raise SignedUrlUnavailable, "issuer (client_email) missing" unless issuer
92
195
  issuer
93
196
  end
94
197
 
95
198
  def determine_signing_key signing_key, private_key
96
- signing_key = signing_key || private_key ||
97
- @service.credentials.signing_key
98
- unless signing_key
99
- raise SignedUrlUnavailable, "signing_key (private_key) missing"
100
- end
199
+ signing_key = signing_key || private_key || @service.credentials.signing_key
200
+ raise SignedUrlUnavailable, "signing_key (private_key) missing" unless signing_key
101
201
  signing_key
102
202
  end
103
203
 
104
204
  def service_account_signer signer
105
- unless signer.respond_to? :sign
106
- signer = OpenSSL::PKey::RSA.new signer
107
- end
205
+ signer = OpenSSL::PKey::RSA.new signer unless signer.respond_to? :sign
108
206
  # Sign string to sign
109
207
  lambda do |string_to_sign|
110
208
  sig = signer.sign OpenSSL::Digest::SHA256.new, string_to_sign
@@ -119,25 +217,22 @@ module Google
119
217
  [issuer, signer]
120
218
  end
121
219
 
122
- def canonical_and_signed_headers headers
123
- # Headers needs to be in alpha order.
220
+ def canonical_and_signed_headers headers, virtual_hosted_style, bucket_bound_hostname
221
+ if virtual_hosted_style && bucket_bound_hostname
222
+ raise "virtual_hosted_style: #{virtual_hosted_style} and bucket_bound_hostname: " \
223
+ "#{bucket_bound_hostname} params cannot both be passed together"
224
+ end
225
+
124
226
  canonical_headers = headers || {}
125
227
  headers_arr = canonical_headers.map do |k, v|
126
- [k.downcase, v.strip.gsub(/[^\S\t]+/, " ").gsub(/\t+/, "\t")]
228
+ [k.downcase, v.strip.gsub(/[^\S\t]+/, " ").gsub(/\t+/, " ")]
127
229
  end
128
230
  canonical_headers = Hash[headers_arr]
129
- canonical_headers["host"] = "storage.googleapis.com"
130
-
131
- canonical_headers = canonical_headers.sort_by do |k, _|
132
- k.downcase
133
- end.to_h
134
- canonical_headers_str = ""
135
- canonical_headers.each do |k, v|
136
- canonical_headers_str += "#{k}:#{v}\n"
137
- end
138
- signed_headers_str = ""
139
- canonical_headers.each_key { |k| signed_headers_str += "#{k};" }
140
- signed_headers_str = signed_headers_str.chomp ";"
231
+ canonical_headers["host"] = host_name virtual_hosted_style, bucket_bound_hostname
232
+
233
+ canonical_headers = canonical_headers.sort_by(&:first).to_h
234
+ canonical_headers_str = canonical_headers.map { |k, v| "#{k}:#{v}\n" }.join
235
+ signed_headers_str = canonical_headers.keys.join ";"
141
236
  [canonical_headers_str, signed_headers_str]
142
237
  end
143
238
 
@@ -149,32 +244,103 @@ module Google
149
244
  expires
150
245
  end
151
246
 
152
- def canonical_query query, algorithm, credential, goog_date, expires,
153
- signed_headers_str
247
+ def canonical_query query, algorithm, credential, goog_date, expires, signed_headers_str
154
248
  query ||= {}
155
249
  query["X-Goog-Algorithm"] = algorithm
156
250
  query["X-Goog-Credential"] = credential
157
251
  query["X-Goog-Date"] = goog_date
158
252
  query["X-Goog-Expires"] = expires
159
- query["X-Goog-SignedHeaders"] = CGI.escape signed_headers_str
160
- query = query.sort_by { |k, _| k.to_s.downcase }.to_h
161
- canonical_query_str = ""
162
- query.each { |k, v| canonical_query_str += "#{k}=#{v}&" }
163
- canonical_query_str.chomp "&"
253
+ query["X-Goog-SignedHeaders"] = signed_headers_str
254
+ query = query.map { |k, v| [escape_query_param(k), escape_query_param(v)] }.sort_by(&:first).to_h
255
+ query.map { |k, v| "#{k}=#{v}" }.join "&"
256
+ end
257
+
258
+ ##
259
+ # Only the characters in the regex set [A-Za-z0-9.~_-] must be left un-escaped; all others must be
260
+ # percent-encoded using %XX UTF-8 style.
261
+ def escape_query_param str
262
+ CGI.escape(str.to_s).gsub("%7E", "~")
263
+ end
264
+
265
+ def host_name virtual_hosted_style, bucket_bound_hostname
266
+ return bucket_bound_hostname if bucket_bound_hostname
267
+ virtual_hosted_style ? "#{@bucket_name}.storage.googleapis.com" : "storage.googleapis.com"
164
268
  end
165
269
 
166
270
  ##
167
271
  # The URI-encoded (percent encoded) external path to the file.
168
- def ext_path
169
- path = "/#{@bucket_name}"
170
- path += "/#{String(@file_name)}" if @file_name && !@file_name.empty?
171
- Addressable::URI.escape path
272
+ def file_path path_style
273
+ path = []
274
+ path << "/#{@bucket_name}" if path_style
275
+ path << "/#{String(@file_name)}" if @file_name && !@file_name.empty?
276
+ CGI.escape(path.join).gsub "%2F", "/"
277
+ end
278
+
279
+ ##
280
+ # The external path to the bucket, with trailing slash.
281
+ def bucket_path path_style
282
+ return "/#{@bucket_name}/" if path_style
172
283
  end
173
284
 
174
285
  ##
175
286
  # The external url to the file.
176
- def ext_url
177
- "#{GOOGLEAPIS_URL}#{ext_path}"
287
+ def ext_url scheme, virtual_hosted_style, bucket_bound_hostname
288
+ url = GOOGLEAPIS_URL.dup
289
+ if virtual_hosted_style
290
+ parts = url.split "//"
291
+ parts[1] = "#{@bucket_name}.#{parts[1]}"
292
+ parts.join "//"
293
+ elsif bucket_bound_hostname
294
+ raise ArgumentError, "scheme is required" unless scheme
295
+ URI "#{scheme.to_s.downcase}://#{bucket_bound_hostname}"
296
+ else
297
+ url
298
+ end
299
+ end
300
+
301
+ def path_style? virtual_hosted_style, bucket_bound_hostname
302
+ !(virtual_hosted_style || bucket_bound_hostname)
303
+ end
304
+
305
+ ##
306
+ # The external path to the file, URI-encoded.
307
+ # Will not URI encode the special `${filename}` variable.
308
+ # "You can also use the ${filename} variable..."
309
+ # https://cloud.google.com/storage/docs/xml-api/post-object
310
+ #
311
+ def post_object_ext_path
312
+ path = "/#{@bucket_name}/#{@file_name}"
313
+ escaped = Addressable::URI.escape path
314
+ special_var = "${filename}"
315
+ # Restore the unencoded `${filename}` variable, if present.
316
+ if path.include? special_var
317
+ return escaped.gsub "$%7Bfilename%7D", special_var
318
+ end
319
+ escaped
320
+ end
321
+
322
+ ##
323
+ # The external url to the file.
324
+ def post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname
325
+ url = GOOGLEAPIS_URL.dup
326
+ if virtual_hosted_style
327
+ parts = url.split "//"
328
+ parts[1] = "#{@bucket_name}.#{parts[1]}/"
329
+ parts.join "//"
330
+ elsif bucket_bound_hostname
331
+ raise ArgumentError, "scheme is required" unless scheme
332
+ URI "#{scheme.to_s.downcase}://#{bucket_bound_hostname}/"
333
+ else
334
+ url
335
+ end
336
+ end
337
+
338
+ def generate_signature signing_key, data
339
+ unless signing_key.respond_to? :sign
340
+ signing_key = OpenSSL::PKey::RSA.new signing_key
341
+ end
342
+ signature = signing_key.sign OpenSSL::Digest::SHA256.new, data
343
+ signature.unpack("H*").first.force_encoding "utf-8"
178
344
  end
179
345
  end
180
346
  end
@@ -230,11 +230,12 @@ module Google
230
230
  ##
231
231
  # @private
232
232
  def to_gapi
233
- Google::Apis::StorageV1::Policy::Binding.new({
233
+ params = {
234
234
  role: @role,
235
235
  members: @members,
236
236
  condition: @condition&.to_gapi
237
- }.delete_if { |_, v| v.nil? })
237
+ }.delete_if { |_, v| v.nil? }
238
+ Google::Apis::StorageV1::Policy::Binding.new(**params)
238
239
  end
239
240
  end
240
241
  end
@@ -27,7 +27,7 @@ module Google
27
27
  # form. Each key/value pair should be set as an input tag's name and
28
28
  # value.
29
29
  #
30
- # @example
30
+ # @example Using Bucket#post_object (V2):
31
31
  # require "google/cloud/storage"
32
32
  #
33
33
  # storage = Google::Cloud::Storage.new
@@ -41,6 +41,23 @@ module Google
41
41
  # post.fields[:signature] #=> "ABC...XYZ="
42
42
  # post.fields[:policy] #=> "ABC...XYZ="
43
43
  #
44
+ # @example Using Bucket#generate_signed_post_policy_v4 (V4):
45
+ # require "google/cloud/storage"
46
+ #
47
+ # storage = Google::Cloud::Storage.new
48
+ #
49
+ # bucket = storage.bucket "my-todo-app"
50
+ # conditions = [["starts-with","$acl","public"]]
51
+ # post = bucket.generate_signed_post_policy_v4 "avatars/heidi/400x400.png", expires: 10, conditions: conditions
52
+ #
53
+ # post.url #=> "https://storage.googleapis.com/my-todo-app/"
54
+ # post.fields["key"] #=> "my-todo-app/avatars/heidi/400x400.png"
55
+ # post.fields["policy"] #=> "ABC...XYZ"
56
+ # post.fields["x-goog-algorithm"] #=> "GOOG4-RSA-SHA256"
57
+ # post.fields["x-goog-credential"] #=> "cred@pid.iam.gserviceaccount.com/20200123/auto/storage/goog4_request"
58
+ # post.fields["x-goog-date"] #=> "20200128T000000Z"
59
+ # post.fields["x-goog-signature"] #=> "4893a0e...cd82"
60
+ #
44
61
  class PostObject
45
62
  attr_reader :url, :fields
46
63
 
@@ -357,10 +357,11 @@ module Google
357
357
  logging_bucket: nil, logging_prefix: nil,
358
358
  website_main: nil, website_404: nil, versioning: nil,
359
359
  requester_pays: nil, user_project: nil
360
- new_bucket = Google::Apis::StorageV1::Bucket.new({
360
+ params = {
361
361
  name: bucket_name,
362
362
  location: location
363
- }.delete_if { |_, v| v.nil? })
363
+ }.delete_if { |_, v| v.nil? }
364
+ new_bucket = Google::Apis::StorageV1::Bucket.new(**params)
364
365
  storage_class = storage_class_for storage_class
365
366
  updater = Bucket::Updater.new(new_bucket).tap do |b|
366
367
  b.logging_bucket = logging_bucket unless logging_bucket.nil?
@@ -522,6 +523,19 @@ module Google
522
523
  # using the URL, but only when the file resource is missing the
523
524
  # corresponding values. (These values can be permanently set using
524
525
  # {File#content_disposition=} and {File#content_type=}.)
526
+ # @param [String] scheme The URL scheme. The default value is `HTTPS`.
527
+ # @param [Boolean] virtual_hosted_style Whether to use a virtual hosted-style
528
+ # hostname, which adds the bucket into the host portion of the URI rather
529
+ # than the path, e.g. `https://mybucket.storage.googleapis.com/...`.
530
+ # For V4 signing, this also sets the `host` header in the canonicalized
531
+ # extension headers to the virtual hosted-style host, unless that header is
532
+ # supplied via the `headers` param. The default value of `false` uses the
533
+ # form of `https://storage.googleapis.com/mybucket`.
534
+ # @param [String] bucket_bound_hostname Use a bucket-bound hostname, which
535
+ # replaces the `storage.googleapis.com` host with the name of a `CNAME`
536
+ # bucket, e.g. a bucket named `gcs-subdomain.my.domain.tld`, or a Google
537
+ # Cloud Load Balancer which routes to a bucket you own, e.g.
538
+ # `my-load-balancer-domain.tld`.
525
539
  # @param [Symbol, String] version The version of the signed credential
526
540
  # to create. Must be one of `:v2` or `:v4`. The default value is
527
541
  # `:v2`.
@@ -591,28 +605,50 @@ module Google
591
605
  # # Send the `x-goog-resumable:start` header and the content type
592
606
  # # with the resumable upload POST request.
593
607
  #
594
- def signed_url bucket, path, method: nil, expires: nil,
595
- content_type: nil, content_md5: nil, headers: nil,
596
- issuer: nil, client_email: nil, signing_key: nil,
597
- private_key: nil, query: nil, version: nil
608
+ def signed_url bucket,
609
+ path,
610
+ method: "GET",
611
+ expires: nil,
612
+ content_type: nil,
613
+ content_md5: nil,
614
+ headers: nil,
615
+ issuer: nil,
616
+ client_email: nil,
617
+ signing_key: nil,
618
+ private_key: nil,
619
+ query: nil,
620
+ scheme: "HTTPS",
621
+ virtual_hosted_style: nil,
622
+ bucket_bound_hostname: nil,
623
+ version: nil
598
624
  version ||= :v2
599
625
  case version.to_sym
600
626
  when :v2
601
627
  signer = File::SignerV2.new bucket, path, service
602
628
 
603
- signer.signed_url method: method, expires: expires,
604
- headers: headers, content_type: content_type,
605
- content_md5: content_md5, issuer: issuer,
629
+ signer.signed_url method: method,
630
+ expires: expires,
631
+ headers: headers,
632
+ content_type: content_type,
633
+ content_md5: content_md5,
634
+ issuer: issuer,
606
635
  client_email: client_email,
607
636
  signing_key: signing_key,
608
- private_key: private_key, query: query
637
+ private_key: private_key,
638
+ query: query
609
639
  when :v4
610
640
  signer = File::SignerV4.new bucket, path, service
611
- signer.signed_url method: method, expires: expires,
612
- headers: headers, issuer: issuer,
641
+ signer.signed_url method: method,
642
+ expires: expires,
643
+ headers: headers,
644
+ issuer: issuer,
613
645
  client_email: client_email,
614
646
  signing_key: signing_key,
615
- private_key: private_key, query: query
647
+ private_key: private_key,
648
+ query: query,
649
+ scheme: scheme,
650
+ virtual_hosted_style: virtual_hosted_style,
651
+ bucket_bound_hostname: bucket_bound_hostname
616
652
  else
617
653
  raise ArgumentError, "version '#{version}' not supported"
618
654
  end
@@ -152,9 +152,8 @@ module Google
152
152
  ##
153
153
  # Creates a new bucket ACL.
154
154
  def insert_bucket_acl bucket_name, entity, role, user_project: nil
155
- new_acl = Google::Apis::StorageV1::BucketAccessControl.new(
156
- { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
157
- )
155
+ params = { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
156
+ new_acl = Google::Apis::StorageV1::BucketAccessControl.new(**params)
158
157
  execute do
159
158
  service.insert_bucket_access_control \
160
159
  bucket_name, new_acl, user_project: user_project(user_project)
@@ -182,9 +181,8 @@ module Google
182
181
  ##
183
182
  # Creates a new default ACL.
184
183
  def insert_default_acl bucket_name, entity, role, user_project: nil
185
- new_acl = Google::Apis::StorageV1::ObjectAccessControl.new(
186
- { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
187
- )
184
+ param = { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
185
+ new_acl = Google::Apis::StorageV1::ObjectAccessControl.new(**param)
188
186
  execute do
189
187
  service.insert_default_object_access_control \
190
188
  bucket_name, new_acl, user_project: user_project(user_project)
@@ -243,13 +241,13 @@ module Google
243
241
  def insert_notification bucket_name, topic_name, custom_attrs: nil,
244
242
  event_types: nil, prefix: nil, payload: nil,
245
243
  user_project: nil
246
- new_notification = Google::Apis::StorageV1::Notification.new(
244
+ params =
247
245
  { custom_attributes: custom_attrs,
248
246
  event_types: event_types(event_types),
249
247
  object_name_prefix: prefix,
250
248
  payload_format: payload_format(payload),
251
249
  topic: topic_path(topic_name) }.delete_if { |_k, v| v.nil? }
252
- )
250
+ new_notification = Google::Apis::StorageV1::Notification.new(**params)
253
251
 
254
252
  execute do
255
253
  service.insert_notification \
@@ -298,14 +296,14 @@ module Google
298
296
  storage_class: nil, key: nil, kms_key: nil,
299
297
  temporary_hold: nil, event_based_hold: nil,
300
298
  user_project: nil
301
- file_obj = Google::Apis::StorageV1::Object.new(
299
+ params =
302
300
  { cache_control: cache_control, content_type: content_type,
303
301
  content_disposition: content_disposition, md5_hash: md5,
304
302
  content_encoding: content_encoding, crc32c: crc32c,
305
303
  content_language: content_language, metadata: metadata,
306
304
  storage_class: storage_class, temporary_hold: temporary_hold,
307
305
  event_based_hold: event_based_hold }.delete_if { |_k, v| v.nil? }
308
- )
306
+ file_obj = Google::Apis::StorageV1::Object.new(**params)
309
307
  content_type ||= mime_type_for(path || Pathname(source).to_path)
310
308
 
311
309
  execute do
@@ -432,9 +430,8 @@ module Google
432
430
  # Creates a new file ACL.
433
431
  def insert_file_acl bucket_name, file_name, entity, role,
434
432
  generation: nil, user_project: nil
435
- new_acl = Google::Apis::StorageV1::ObjectAccessControl.new(
436
- { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
437
- )
433
+ params = { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
434
+ new_acl = Google::Apis::StorageV1::ObjectAccessControl.new(**params)
438
435
  execute do
439
436
  service.insert_object_access_control \
440
437
  bucket_name, file_name, new_acl,
@@ -16,7 +16,7 @@
16
16
  module Google
17
17
  module Cloud
18
18
  module Storage
19
- VERSION = "1.25.1".freeze
19
+ VERSION = "1.26.0".freeze
20
20
  end
21
21
  end
22
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google-cloud-storage
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.25.1
4
+ version: 1.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Moore
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-01-06 00:00:00.000000000 Z
12
+ date: 2020-04-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: google-cloud-core