Skip to content

Commit db7ad30

Browse files
authored
feat: Add Auth Context enabled Firestore triggers (#184)
1 parent 37c1fc0 commit db7ad30

File tree

2 files changed

+327
-12
lines changed

2 files changed

+327
-12
lines changed

src/firebase_functions/firestore_fn.py

+255-12
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
_event_type_updated = "google.cloud.firestore.document.v1.updated"
3939
_event_type_deleted = "google.cloud.firestore.document.v1.deleted"
4040

41+
_event_type_written_with_auth_context = "google.cloud.firestore.document.v1.written.withAuthContext"
42+
_event_type_created_with_auth_context = "google.cloud.firestore.document.v1.created.withAuthContext"
43+
_event_type_updated_with_auth_context = "google.cloud.firestore.document.v1.updated.withAuthContext"
44+
_event_type_deleted_with_auth_context = "google.cloud.firestore.document.v1.deleted.withAuthContext"
45+
4146

4247
@_dataclass.dataclass(frozen=True)
4348
class Event(_core.CloudEvent[_core.T]):
@@ -82,9 +87,26 @@ class Event(_core.CloudEvent[_core.T]):
8287
_C1 = _typing.Callable[[_E1], None]
8388
_C2 = _typing.Callable[[_E2], None]
8489

90+
AuthType = _typing.Literal["service_account", "api_key", "system",
91+
"unauthenticated", "unknown"]
92+
93+
94+
@_dataclass.dataclass(frozen=True)
95+
class AuthEvent(Event[_core.T]):
96+
auth_type: AuthType
97+
"""The type of principal that triggered the event"""
98+
auth_id: str | None
99+
"""The unique identifier for the principal"""
100+
101+
102+
_E3 = AuthEvent[Change[DocumentSnapshot | None]]
103+
_E4 = AuthEvent[DocumentSnapshot | None]
104+
_C3 = _typing.Callable[[_E3], None]
105+
_C4 = _typing.Callable[[_E4], None]
106+
85107

86108
def _firestore_endpoint_handler(
87-
func: _C1 | _C2,
109+
func: _C1 | _C2 | _C3 | _C4,
88110
event_type: str,
89111
document_pattern: _path_pattern.PathPattern,
90112
raw: _ce.CloudEvent,
@@ -94,12 +116,14 @@ def _firestore_endpoint_handler(
94116
firestore_event_data: _firestore.DocumentEventData
95117
content_type: str = event_attributes["datacontenttype"]
96118
if "application/json" in content_type or isinstance(event_data, dict):
97-
firestore_event_data = _firestore.DocumentEventData.from_json(
98-
event_data)
119+
firestore_event_data = _typing.cast(
120+
_firestore.DocumentEventData,
121+
_firestore.DocumentEventData.from_json(event_data))
99122
elif "application/protobuf" in content_type or isinstance(
100123
event_data, bytes):
101-
firestore_event_data = _firestore.DocumentEventData.deserialize(
102-
event_data)
124+
firestore_event_data = _typing.cast(
125+
_firestore.DocumentEventData,
126+
_firestore.DocumentEventData.deserialize(event_data))
103127
else:
104128
actual_type = type(event_data)
105129
raise TypeError(f"Firestore: Cannot parse event payload of data type "
@@ -110,6 +134,8 @@ def _firestore_endpoint_handler(
110134
event_namespace = event_attributes["namespace"]
111135
event_document = event_attributes["document"]
112136
event_database = event_attributes["database"]
137+
event_auth_type = event_attributes["authtype"]
138+
event_auth_id = event_attributes["authid"]
113139

114140
time = event_attributes["time"]
115141
event_time = _util.timestamp_conversion(time)
@@ -146,18 +172,23 @@ def _firestore_endpoint_handler(
146172
firestore_event_data.old_value.update_time,
147173
)
148174
if event_type == _event_type_deleted:
149-
firestore_event_data = old_value_snapshot
175+
firestore_event_data = _typing.cast(_firestore.DocumentEventData,
176+
old_value_snapshot)
150177
if event_type == _event_type_created:
151-
firestore_event_data = value_snapshot
178+
firestore_event_data = _typing.cast(_firestore.DocumentEventData,
179+
value_snapshot)
152180
if event_type in (_event_type_written, _event_type_updated):
153-
firestore_event_data = Change(
154-
before=old_value_snapshot,
155-
after=value_snapshot,
156-
)
181+
firestore_event_data = _typing.cast(
182+
_firestore.DocumentEventData,
183+
Change(
184+
before=old_value_snapshot,
185+
after=value_snapshot,
186+
))
157187

158188
params: dict[str, str] = {
159189
**document_pattern.extract_matches(event_document),
160190
}
191+
161192
database_event = Event(
162193
project=event_project,
163194
namespace=event_namespace,
@@ -173,7 +204,15 @@ def _firestore_endpoint_handler(
173204
subject=event_attributes["subject"],
174205
params=params,
175206
)
176-
func(database_event)
207+
208+
if event_type.endswith(".withAuthContext"):
209+
database_event_with_auth_context = AuthEvent(**vars(database_event),
210+
auth_type=event_auth_type,
211+
auth_id=event_auth_id)
212+
func(database_event_with_auth_context)
213+
else:
214+
# mypy cannot infer that the event type is correct, hence the cast
215+
_typing.cast(_C1 | _C2, func)(database_event)
177216

178217

179218
@_util.copy_func_kwargs(FirestoreOptions)
@@ -224,6 +263,57 @@ def on_document_written_wrapped(raw: _ce.CloudEvent):
224263
return on_document_written_inner_decorator
225264

226265

266+
@_util.copy_func_kwargs(FirestoreOptions)
267+
def on_document_written_with_auth_context(**kwargs
268+
) -> _typing.Callable[[_C1], _C1]:
269+
"""
270+
Event handler that triggers when a document is created, updated, or deleted in Firestore.
271+
This trigger will also provide the authentication context of the principal who triggered
272+
the event.
273+
274+
Example:
275+
276+
.. code-block:: python
277+
278+
@on_document_written_with_auth_context(document="*")
279+
def example(event: AuthEvent[Change[DocumentSnapshot]]) -> None:
280+
pass
281+
282+
:param \\*\\*kwargs: Firestore options.
283+
:type \\*\\*kwargs: as :exc:`firebase_functions.options.FirestoreOptions`
284+
:rtype: :exc:`typing.Callable`
285+
\\[ \\[ :exc:`firebase_functions.firestore_fn.AuthEvent` \\[
286+
:exc:`firebase_functions.db.Change` \\] \\], `None` \\]
287+
A function that takes a Firestore event and returns ``None``.
288+
"""
289+
options = FirestoreOptions(**kwargs)
290+
291+
def on_document_written_with_auth_context_inner_decorator(func: _C1):
292+
document_pattern = _path_pattern.PathPattern(
293+
_util.normalize_path(options.document))
294+
295+
@_functools.wraps(func)
296+
def on_document_written_with_auth_context_wrapped(raw: _ce.CloudEvent):
297+
return _firestore_endpoint_handler(
298+
func,
299+
_event_type_written_with_auth_context,
300+
document_pattern,
301+
raw,
302+
)
303+
304+
_util.set_func_endpoint_attr(
305+
on_document_written_with_auth_context_wrapped,
306+
options._endpoint(
307+
event_type=_event_type_written,
308+
func_name=func.__name__,
309+
document_pattern=document_pattern,
310+
),
311+
)
312+
return on_document_written_with_auth_context_wrapped
313+
314+
return on_document_written_with_auth_context_inner_decorator
315+
316+
227317
@_util.copy_func_kwargs(FirestoreOptions)
228318
def on_document_updated(**kwargs) -> _typing.Callable[[_C1], _C1]:
229319
"""
@@ -272,6 +362,57 @@ def on_document_updated_wrapped(raw: _ce.CloudEvent):
272362
return on_document_updated_inner_decorator
273363

274364

365+
@_util.copy_func_kwargs(FirestoreOptions)
366+
def on_document_updated_with_auth_context(**kwargs
367+
) -> _typing.Callable[[_C1], _C1]:
368+
"""
369+
Event handler that triggers when a document is updated in Firestore.
370+
This trigger will also provide the authentication context of the principal who triggered
371+
the event.
372+
373+
Example:
374+
375+
.. code-block:: python
376+
377+
@on_document_updated_with_auth_context(document="*")
378+
def example(event: AuthEvent[Change[DocumentSnapshot]]) -> None:
379+
pass
380+
381+
:param \\*\\*kwargs: Firestore options.
382+
:type \\*\\*kwargs: as :exc:`firebase_functions.options.FirestoreOptions`
383+
:rtype: :exc:`typing.Callable`
384+
\\[ \\[ :exc:`firebase_functions.firestore_fn.AuthEvent` \\[
385+
:exc:`firebase_functions.db.Change` \\] \\], `None` \\]
386+
A function that takes a Firestore event and returns ``None``.
387+
"""
388+
options = FirestoreOptions(**kwargs)
389+
390+
def on_document_updated_with_auth_context_inner_decorator(func: _C1):
391+
document_pattern = _path_pattern.PathPattern(
392+
_util.normalize_path(options.document))
393+
394+
@_functools.wraps(func)
395+
def on_document_updated_with_auth_context_wrapped(raw: _ce.CloudEvent):
396+
return _firestore_endpoint_handler(
397+
func,
398+
_event_type_updated_with_auth_context,
399+
document_pattern,
400+
raw,
401+
)
402+
403+
_util.set_func_endpoint_attr(
404+
on_document_updated_with_auth_context_wrapped,
405+
options._endpoint(
406+
event_type=_event_type_updated_with_auth_context,
407+
func_name=func.__name__,
408+
document_pattern=document_pattern,
409+
),
410+
)
411+
return on_document_updated_with_auth_context_wrapped
412+
413+
return on_document_updated_with_auth_context_inner_decorator
414+
415+
275416
@_util.copy_func_kwargs(FirestoreOptions)
276417
def on_document_created(**kwargs) -> _typing.Callable[[_C2], _C2]:
277418
"""
@@ -320,6 +461,57 @@ def on_document_created_wrapped(raw: _ce.CloudEvent):
320461
return on_document_created_inner_decorator
321462

322463

464+
@_util.copy_func_kwargs(FirestoreOptions)
465+
def on_document_created_with_auth_context(**kwargs
466+
) -> _typing.Callable[[_C2], _C2]:
467+
"""
468+
Event handler that triggers when a document is created in Firestore.
469+
This trigger will also provide the authentication context of the principal who triggered
470+
the event.
471+
472+
Example:
473+
474+
.. code-block:: python
475+
476+
@on_document_created_with_auth_context(document="*")
477+
def example(event: AuthEvent[DocumentSnapshot]):
478+
pass
479+
480+
:param \\*\\*kwargs: Firestore options.
481+
:type \\*\\*kwargs: as :exc:`firebase_functions.options.FirestoreOptions`
482+
:rtype: :exc:`typing.Callable`
483+
\\[ \\[ :exc:`firebase_functions.firestore_fn.AuthEvent` \\[
484+
:exc:`object` \\] \\], `None` \\]
485+
A function that takes a Firestore event and returns ``None``.
486+
"""
487+
options = FirestoreOptions(**kwargs)
488+
489+
def on_document_created_with_auth_context_inner_decorator(func: _C2):
490+
document_pattern = _path_pattern.PathPattern(
491+
_util.normalize_path(options.document))
492+
493+
@_functools.wraps(func)
494+
def on_document_created_with_auth_context_wrapped(raw: _ce.CloudEvent):
495+
return _firestore_endpoint_handler(
496+
func,
497+
_event_type_created_with_auth_context,
498+
document_pattern,
499+
raw,
500+
)
501+
502+
_util.set_func_endpoint_attr(
503+
on_document_created_with_auth_context_wrapped,
504+
options._endpoint(
505+
event_type=_event_type_created_with_auth_context,
506+
func_name=func.__name__,
507+
document_pattern=document_pattern,
508+
),
509+
)
510+
return on_document_created_with_auth_context_wrapped
511+
512+
return on_document_created_with_auth_context_inner_decorator
513+
514+
323515
@_util.copy_func_kwargs(FirestoreOptions)
324516
def on_document_deleted(**kwargs) -> _typing.Callable[[_C2], _C2]:
325517
"""
@@ -366,3 +558,54 @@ def on_document_deleted_wrapped(raw: _ce.CloudEvent):
366558
return on_document_deleted_wrapped
367559

368560
return on_document_deleted_inner_decorator
561+
562+
563+
@_util.copy_func_kwargs(FirestoreOptions)
564+
def on_document_deleted_with_auth_context(**kwargs
565+
) -> _typing.Callable[[_C2], _C2]:
566+
"""
567+
Event handler that triggers when a document is deleted in Firestore.
568+
This trigger will also provide the authentication context of the principal who triggered
569+
the event.
570+
571+
Example:
572+
573+
.. code-block:: python
574+
575+
@on_document_deleted_with_auth_context(document="*")
576+
def example(event: AuthEvent[DocumentSnapshot]) -> None:
577+
pass
578+
579+
:param \\*\\*kwargs: Firestore options.
580+
:type \\*\\*kwargs: as :exc:`firebase_functions.options.FirestoreOptions`
581+
:rtype: :exc:`typing.Callable`
582+
\\[ \\[ :exc:`firebase_functions.firestore_fn.AuthEvent` \\[
583+
:exc:`object` \\] \\], `None` \\]
584+
A function that takes a Firestore event and returns ``None``.
585+
"""
586+
options = FirestoreOptions(**kwargs)
587+
588+
def on_document_deleted_with_auth_context_inner_decorator(func: _C2):
589+
document_pattern = _path_pattern.PathPattern(
590+
_util.normalize_path(options.document))
591+
592+
@_functools.wraps(func)
593+
def on_document_deleted_with_auth_context_wrapped(raw: _ce.CloudEvent):
594+
return _firestore_endpoint_handler(
595+
func,
596+
_event_type_deleted_with_auth_context,
597+
document_pattern,
598+
raw,
599+
)
600+
601+
_util.set_func_endpoint_attr(
602+
on_document_deleted_with_auth_context_wrapped,
603+
options._endpoint(
604+
event_type=_event_type_deleted_with_auth_context,
605+
func_name=func.__name__,
606+
document_pattern=document_pattern,
607+
),
608+
)
609+
return on_document_deleted_with_auth_context_wrapped
610+
611+
return on_document_deleted_with_auth_context_inner_decorator

0 commit comments

Comments
 (0)