|
14 | 14 |
|
15 | 15 | """Client for interacting with the Google Cloud Storage API.""" |
16 | 16 |
|
17 | | -import warnings |
| 17 | +import base64 |
| 18 | +import binascii |
| 19 | +import datetime |
18 | 20 | import functools |
| 21 | +import json |
| 22 | +import warnings |
19 | 23 | import google.api_core.client_options |
20 | 24 |
|
21 | 25 | from google.auth.credentials import AnonymousCredentials |
22 | 26 |
|
23 | 27 | from google.api_core import page_iterator |
24 | | -from google.cloud._helpers import _LocalStack |
| 28 | +from google.cloud._helpers import _LocalStack, _NOW |
25 | 29 | from google.cloud.client import ClientWithProject |
26 | 30 | from google.cloud.exceptions import NotFound |
27 | 31 | from google.cloud.storage._helpers import _get_storage_host |
28 | 32 | from google.cloud.storage._http import Connection |
| 33 | +from google.cloud.storage._signing import ( |
| 34 | + get_expiration_seconds_v4, |
| 35 | + get_v4_now_dtstamps, |
| 36 | + ensure_signed_credentials, |
| 37 | + _sign_message, |
| 38 | +) |
29 | 39 | from google.cloud.storage.batch import Batch |
30 | 40 | from google.cloud.storage.bucket import Bucket |
31 | 41 | from google.cloud.storage.blob import Blob |
@@ -836,6 +846,174 @@ def get_hmac_key_metadata( |
836 | 846 | metadata.reload(timeout=timeout) # raises NotFound for missing key |
837 | 847 | return metadata |
838 | 848 |
|
| 849 | + def generate_signed_post_policy_v4( |
| 850 | + self, |
| 851 | + bucket_name, |
| 852 | + blob_name, |
| 853 | + expiration, |
| 854 | + conditions=None, |
| 855 | + fields=None, |
| 856 | + credentials=None, |
| 857 | + virtual_hosted_style=False, |
| 858 | + bucket_bound_hostname=None, |
| 859 | + scheme=None, |
| 860 | + service_account_email=None, |
| 861 | + access_token=None, |
| 862 | + ): |
| 863 | + """Generate a V4 signed policy object. |
| 864 | +
|
| 865 | + .. note:: |
| 866 | +
|
| 867 | + Assumes ``credentials`` implements the |
| 868 | + :class:`google.auth.credentials.Signing` interface. Also assumes |
| 869 | + ``credentials`` has a ``service_account_email`` property which |
| 870 | + identifies the credentials. |
| 871 | +
|
| 872 | + Generated policy object allows user to upload objects with a POST request. |
| 873 | +
|
| 874 | + :type bucket_name: str |
| 875 | + :param bucket_name: Bucket name. |
| 876 | +
|
| 877 | + :type blob_name: str |
| 878 | + :param blob_name: Object name. |
| 879 | +
|
| 880 | + :type expiration: Union[Integer, datetime.datetime, datetime.timedelta] |
| 881 | + :param expiration: Policy expiration time. |
| 882 | +
|
| 883 | + :type conditions: list |
| 884 | + :param conditions: (Optional) List of POST policy conditions, which are |
| 885 | + used to restrict what is allowed in the request. |
| 886 | +
|
| 887 | + :type fields: dict |
| 888 | + :param fields: (Optional) Additional elements to include into request. |
| 889 | +
|
| 890 | + :type credentials: :class:`google.auth.credentials.Signing` |
| 891 | + :param credentials: (Optional) Credentials object with an associated private |
| 892 | + key to sign text. |
| 893 | +
|
| 894 | + :type virtual_hosted_style: bool |
| 895 | + :param virtual_hosted_style: (Optional) If True, construct the URL relative to the bucket |
| 896 | + virtual hostname, e.g., '<bucket-name>.storage.googleapis.com'. |
| 897 | +
|
| 898 | + :type bucket_bound_hostname: str |
| 899 | + :param bucket_bound_hostname: |
| 900 | + (Optional) If passed, construct the URL relative to the bucket-bound hostname. |
| 901 | + Value can be bare or with a scheme, e.g., 'example.com' or 'http://example.com'. |
| 902 | + See: https://cloud.google.com/storage/docs/request-endpoints#cname |
| 903 | +
|
| 904 | + :type scheme: str |
| 905 | + :param scheme: |
| 906 | + (Optional) If ``bucket_bound_hostname`` is passed as a bare hostname, use |
| 907 | + this value as a scheme. ``https`` will work only when using a CDN. |
| 908 | + Defaults to ``"http"``. |
| 909 | +
|
| 910 | + :type service_account_email: str |
| 911 | + :param service_account_email: (Optional) E-mail address of the service account. |
| 912 | +
|
| 913 | + :type access_token: str |
| 914 | + :param access_token: (Optional) Access token for a service account. |
| 915 | +
|
| 916 | + :rtype: dict |
| 917 | + :returns: Signed POST policy. |
| 918 | +
|
| 919 | + Example: |
| 920 | + Generate signed POST policy and upload a file. |
| 921 | +
|
| 922 | + >>> from google.cloud import storage |
| 923 | + >>> client = storage.Client() |
| 924 | + >>> policy = client.generate_signed_post_policy_v4( |
| 925 | + "bucket-name", |
| 926 | + "blob-name", |
| 927 | + expiration=datetime.datetime(2020, 3, 17), |
| 928 | + conditions=[ |
| 929 | + ["content-length-range", 0, 255] |
| 930 | + ], |
| 931 | + fields=[ |
| 932 | + "x-goog-meta-hello" => "world" |
| 933 | + ], |
| 934 | + ) |
| 935 | + >>> with open("bucket-name", "rb") as f: |
| 936 | + files = {"file": ("bucket-name", f)} |
| 937 | + requests.post(policy["url"], data=policy["fields"], files=files) |
| 938 | + """ |
| 939 | + credentials = self._credentials if credentials is None else credentials |
| 940 | + ensure_signed_credentials(credentials) |
| 941 | + |
| 942 | + # prepare policy conditions and fields |
| 943 | + timestamp, datestamp = get_v4_now_dtstamps() |
| 944 | + |
| 945 | + x_goog_credential = "{email}/{datestamp}/auto/storage/goog4_request".format( |
| 946 | + email=credentials.signer_email, datestamp=datestamp |
| 947 | + ) |
| 948 | + required_conditions = [ |
| 949 | + {"key": blob_name}, |
| 950 | + {"x-goog-date": timestamp}, |
| 951 | + {"x-goog-credential": x_goog_credential}, |
| 952 | + {"x-goog-algorithm": "GOOG4-RSA-SHA256"}, |
| 953 | + ] |
| 954 | + |
| 955 | + conditions = conditions or [] |
| 956 | + policy_fields = {} |
| 957 | + for key, value in sorted((fields or {}).items()): |
| 958 | + if not key.startswith("x-ignore-"): |
| 959 | + policy_fields[key] = value |
| 960 | + conditions.append({key: value}) |
| 961 | + |
| 962 | + conditions += required_conditions |
| 963 | + |
| 964 | + # calculate policy expiration time |
| 965 | + now = _NOW() |
| 966 | + if expiration is None: |
| 967 | + expiration = now + datetime.timedelta(hours=1) |
| 968 | + |
| 969 | + policy_expires = now + datetime.timedelta( |
| 970 | + seconds=get_expiration_seconds_v4(expiration) |
| 971 | + ) |
| 972 | + |
| 973 | + # encode policy for signing |
| 974 | + policy = json.dumps( |
| 975 | + {"conditions": conditions, "expiration": policy_expires.isoformat() + "Z"}, |
| 976 | + separators=(",", ":"), |
| 977 | + ) |
| 978 | + str_to_sign = base64.b64encode(policy.encode("utf-8")) |
| 979 | + |
| 980 | + # sign the policy and get its cryptographic signature |
| 981 | + if access_token and service_account_email: |
| 982 | + signature = _sign_message(str_to_sign, access_token, service_account_email) |
| 983 | + signature_bytes = base64.b64decode(signature) |
| 984 | + else: |
| 985 | + signature_bytes = credentials.sign_bytes(str_to_sign) |
| 986 | + |
| 987 | + # get hexadecimal representation of the signature |
| 988 | + signature = binascii.hexlify(signature_bytes).decode("utf-8") |
| 989 | + |
| 990 | + policy_fields.update( |
| 991 | + { |
| 992 | + "key": blob_name, |
| 993 | + "x-goog-algorithm": "GOOG4-RSA-SHA256", |
| 994 | + "x-goog-credential": x_goog_credential, |
| 995 | + "x-goog-date": timestamp, |
| 996 | + "x-goog-signature": signature, |
| 997 | + "policy": str_to_sign, |
| 998 | + } |
| 999 | + ) |
| 1000 | + # designate URL |
| 1001 | + if virtual_hosted_style: |
| 1002 | + url = "https://{}.storage.googleapis.com/".format(bucket_name) |
| 1003 | + |
| 1004 | + elif bucket_bound_hostname: |
| 1005 | + if ":" in bucket_bound_hostname: # URL includes scheme |
| 1006 | + url = bucket_bound_hostname |
| 1007 | + |
| 1008 | + else: # scheme is given separately |
| 1009 | + url = "{scheme}://{host}/".format( |
| 1010 | + scheme=scheme, host=bucket_bound_hostname |
| 1011 | + ) |
| 1012 | + else: |
| 1013 | + url = "https://storage.googleapis.com/{}/".format(bucket_name) |
| 1014 | + |
| 1015 | + return {"url": url, "fields": policy_fields} |
| 1016 | + |
839 | 1017 |
|
840 | 1018 | def _item_to_bucket(iterator, item): |
841 | 1019 | """Convert a JSON bucket to the native object. |
|
0 commit comments