|
| 1 | +# Copyright 2021 Google LLC |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | +import re |
| 15 | + |
| 16 | +from typing import Tuple |
| 17 | + |
| 18 | +from cloudevents.http import CloudEvent |
| 19 | + |
| 20 | +from functions_framework.background_event import BackgroundEvent |
| 21 | +from functions_framework.exceptions import EventConversionException |
| 22 | +from google.cloud.functions.context import Context |
| 23 | + |
| 24 | +_CLOUDEVENT_SPEC_VERSION = "1.0" |
| 25 | + |
| 26 | +# Maps background/legacy event types to their equivalent CloudEvent types. |
| 27 | +# For more info on event mappings see |
| 28 | +# https://github.com/GoogleCloudPlatform/functions-framework-conformance/blob/master/docs/mapping.md |
| 29 | +_BACKGROUND_TO_CE_TYPE = { |
| 30 | + "google.pubsub.topic.publish": "google.cloud.pubsub.topic.v1.messagePublished", |
| 31 | + "providers/cloud.pubsub/eventTypes/topic.publish": "google.cloud.pubsub.topic.v1.messagePublished", |
| 32 | + "google.storage.object.finalize": "google.cloud.storage.object.v1.finalized", |
| 33 | + "google.storage.object.delete": "google.cloud.storage.object.v1.deleted", |
| 34 | + "google.storage.object.archive": "google.cloud.storage.object.v1.archived", |
| 35 | + "google.storage.object.metadataUpdate": "google.cloud.storage.object.v1.metadataUpdated", |
| 36 | + "providers/cloud.firestore/eventTypes/document.write": "google.cloud.firestore.document.v1.written", |
| 37 | + "providers/cloud.firestore/eventTypes/document.create": "google.cloud.firestore.document.v1.created", |
| 38 | + "providers/cloud.firestore/eventTypes/document.update": "google.cloud.firestore.document.v1.updated", |
| 39 | + "providers/cloud.firestore/eventTypes/document.delete": "google.cloud.firestore.document.v1.deleted", |
| 40 | + "providers/firebase.auth/eventTypes/user.create": "google.firebase.auth.user.v1.created", |
| 41 | + "providers/firebase.auth/eventTypes/user.delete": "google.firebase.auth.user.v1.deleted", |
| 42 | + "providers/google.firebase.analytics/eventTypes/event.log": "google.firebase.analytics.log.v1.written", |
| 43 | + "providers/google.firebase.database/eventTypes/ref.create": "google.firebase.database.document.v1.created", |
| 44 | + "providers/google.firebase.database/eventTypes/ref.write": "google.firebase.database.document.v1.written", |
| 45 | + "providers/google.firebase.database/eventTypes/ref.update": "google.firebase.database.document.v1.updated", |
| 46 | + "providers/google.firebase.database/eventTypes/ref.delete": "google.firebase.database.document.v1.deleted", |
| 47 | + "providers/cloud.storage/eventTypes/object.change": "google.cloud.storage.object.v1.finalized", |
| 48 | +} |
| 49 | + |
| 50 | +# CloudEvent service names. |
| 51 | +_FIREBASE_AUTH_CE_SERVICE = "firebaseauth.googleapis.com" |
| 52 | +_FIREBASE_CE_SERVICE = "firebase.googleapis.com" |
| 53 | +_FIREBASE_DB_CE_SERVICE = "firebasedatabase.googleapis.com" |
| 54 | +_FIRESTORE_CE_SERVICE = "firestore.googleapis.com" |
| 55 | +_PUBSUB_CE_SERVICE = "pubsub.googleapis.com" |
| 56 | +_STORAGE_CE_SERVICE = "storage.googleapis.com" |
| 57 | + |
| 58 | +# Maps background event services to their equivalent CloudEvent services. |
| 59 | +_SERVICE_BACKGROUND_TO_CE = { |
| 60 | + "providers/cloud.firestore/": _FIRESTORE_CE_SERVICE, |
| 61 | + "providers/google.firebase.analytics/": _FIREBASE_CE_SERVICE, |
| 62 | + "providers/firebase.auth/": _FIREBASE_AUTH_CE_SERVICE, |
| 63 | + "providers/google.firebase.database/": _FIREBASE_DB_CE_SERVICE, |
| 64 | + "providers/cloud.pubsub/": _PUBSUB_CE_SERVICE, |
| 65 | + "providers/cloud.storage/": _STORAGE_CE_SERVICE, |
| 66 | + "google.pubsub": _PUBSUB_CE_SERVICE, |
| 67 | + "google.storage": _STORAGE_CE_SERVICE, |
| 68 | +} |
| 69 | + |
| 70 | +# Maps CloudEvent service strings to regular expressions used to split a background |
| 71 | +# event resource string into CloudEvent resource and subject strings. Each regex |
| 72 | +# must have exactly two capture groups: the first for the resource and the second |
| 73 | +# for the subject. |
| 74 | +_CE_SERVICE_TO_RESOURCE_RE = { |
| 75 | + _FIREBASE_CE_SERVICE: re.compile(r"^(projects/[^/]+)/(events/[^/]+)$"), |
| 76 | + _FIREBASE_DB_CE_SERVICE: re.compile(r"^(projects/[^/]/instances/[^/]+)/(refs/.+)$"), |
| 77 | + _FIRESTORE_CE_SERVICE: re.compile( |
| 78 | + r"^(projects/[^/]+/databases/\(default\))/(documents/.+)$" |
| 79 | + ), |
| 80 | + _STORAGE_CE_SERVICE: re.compile(r"^(projects/[^/]/buckets/[^/]+)/(objects/.+)$"), |
| 81 | +} |
| 82 | + |
| 83 | +# Maps Firebase Auth background event metadata field names to their equivalent |
| 84 | +# CloudEvent field names. |
| 85 | +_FIREBASE_AUTH_METADATA_FIELDS_BACKGROUND_TO_CE = { |
| 86 | + "createdAt": "createTime", |
| 87 | + "lastSignedInAt": "lastSignInTime", |
| 88 | +} |
| 89 | + |
| 90 | + |
| 91 | +def background_event_to_cloudevent(request) -> CloudEvent: |
| 92 | + """Converts a background event represented by the given HTTP request into a CloudEvent. """ |
| 93 | + event_data = request.get_json() |
| 94 | + if not event_data: |
| 95 | + raise EventConversionException("Failed to parse JSON") |
| 96 | + |
| 97 | + event_object = BackgroundEvent(**event_data) |
| 98 | + data = event_object.data |
| 99 | + context = Context(**event_object.context) |
| 100 | + |
| 101 | + if context.event_type not in _BACKGROUND_TO_CE_TYPE: |
| 102 | + raise EventConversionException( |
| 103 | + f'Unable to find CloudEvent equivalent type for "{context.event_type}"' |
| 104 | + ) |
| 105 | + new_type = _BACKGROUND_TO_CE_TYPE[context.event_type] |
| 106 | + |
| 107 | + service, resource, subject = _split_resource(context) |
| 108 | + |
| 109 | + # Handle Pub/Sub events. |
| 110 | + if service == _PUBSUB_CE_SERVICE: |
| 111 | + data = {"message": data} |
| 112 | + |
| 113 | + # Handle Firebase Auth events. |
| 114 | + if service == _FIREBASE_AUTH_CE_SERVICE: |
| 115 | + if "metadata" in data: |
| 116 | + for old, new in _FIREBASE_AUTH_METADATA_FIELDS_BACKGROUND_TO_CE.items(): |
| 117 | + if old in data["metadata"]: |
| 118 | + data["metadata"][new] = data["metadata"][old] |
| 119 | + del data["metadata"][old] |
| 120 | + if "uid" in data: |
| 121 | + uid = data["uid"] |
| 122 | + subject = f"users/{uid}" |
| 123 | + |
| 124 | + metadata = { |
| 125 | + "id": context.event_id, |
| 126 | + "time": context.timestamp, |
| 127 | + "specversion": _CLOUDEVENT_SPEC_VERSION, |
| 128 | + "datacontenttype": "application/json", |
| 129 | + "type": new_type, |
| 130 | + "source": f"//{service}/{resource}", |
| 131 | + } |
| 132 | + |
| 133 | + if subject: |
| 134 | + metadata["subject"] = subject |
| 135 | + |
| 136 | + return CloudEvent(metadata, data) |
| 137 | + |
| 138 | + |
| 139 | +def _split_resource(context: Context) -> Tuple[str, str, str]: |
| 140 | + """Splits a background event's resource into a CloudEvent service, resource, and subject.""" |
| 141 | + service = "" |
| 142 | + resource = "" |
| 143 | + if isinstance(context.resource, dict): |
| 144 | + service = context.resource.get("service", "") |
| 145 | + resource = context.resource["name"] |
| 146 | + else: |
| 147 | + resource = context.resource |
| 148 | + |
| 149 | + # If there's no service we'll choose an appropriate one based on the event type. |
| 150 | + if not service: |
| 151 | + for b_service, ce_service in _SERVICE_BACKGROUND_TO_CE.items(): |
| 152 | + if context.event_type.startswith(b_service): |
| 153 | + service = ce_service |
| 154 | + break |
| 155 | + if not service: |
| 156 | + raise EventConversionException( |
| 157 | + "Unable to find CloudEvent equivalent service " |
| 158 | + f"for {context.event_type}" |
| 159 | + ) |
| 160 | + |
| 161 | + # If we don't need to split the resource string then we're done. |
| 162 | + if service not in _CE_SERVICE_TO_RESOURCE_RE: |
| 163 | + return service, resource, "" |
| 164 | + |
| 165 | + # Split resource into resource and subject. |
| 166 | + match = _CE_SERVICE_TO_RESOURCE_RE[service].fullmatch(resource) |
| 167 | + if not match: |
| 168 | + raise EventConversionException("Resource regex did not match") |
| 169 | + |
| 170 | + return service, match.group(1), match.group(2) |
0 commit comments