Skip to content

Commit 23f6a65

Browse files
feat(identity): add v2 Auth triggers (on_user_created and on_user_deleted)
1 parent 4186454 commit 23f6a65

6 files changed

Lines changed: 612 additions & 4 deletions

File tree

‎src/firebase_functions/identity_fn.py‎

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,18 @@
2020
import typing as _typing
2121
from enum import Enum
2222

23+
import cloudevents.http as _ce
2324
from flask import (
2425
Request as _Request,
2526
)
2627
from flask import (
2728
Response as _Response,
2829
)
2930

31+
import firebase_functions.core as _core
3032
import firebase_functions.options as _options
3133
import firebase_functions.private.util as _util
34+
from firebase_functions.options import AuthOptions # noqa: F401
3235

3336

3437
@_dataclasses.dataclass(frozen=True)
@@ -62,10 +65,10 @@ class AuthUserMetadata:
6265
Additional metadata about the user.
6366
"""
6467

65-
creation_time: _dt.datetime
68+
creation_time: _dt.datetime | None = None
6669
"""The date the user was created."""
6770

68-
last_sign_in_time: _dt.datetime | None
71+
last_sign_in_time: _dt.datetime | None = None
6972
"""The date the user last signed in."""
7073

7174

@@ -116,7 +119,7 @@ class AuthMultiFactorSettings:
116119
@_dataclasses.dataclass(frozen=True)
117120
class AuthUserRecord:
118121
"""
119-
The UserRecord passed to auth blocking functions from the identity platform.
122+
The UserRecord passed to auth blocking and event functions from the identity platform.
120123
"""
121124

122125
uid: str
@@ -472,3 +475,120 @@ def before_user_created_wrapped(request: _Request) -> _Response:
472475
return before_user_created_wrapped
473476

474477
return before_user_created_decorator
478+
479+
480+
@_dataclasses.dataclass(frozen=True)
481+
class AuthEvent(_core.CloudEvent[AuthUserRecord]):
482+
"""
483+
A CloudEvent that contains an AuthUserRecord when a user is created or deleted.
484+
"""
485+
486+
project: str | None = None
487+
"""
488+
The project identifier.
489+
"""
490+
491+
tenant_id: str | None = None
492+
"""
493+
The ID of the Identity Platform tenant associated with the event, if applicable.
494+
"""
495+
496+
497+
AuthEventCallable = _typing.Callable[[AuthEvent], None]
498+
"""
499+
The type of the callable for 'on_user_created' and 'on_user_deleted' Auth events.
500+
"""
501+
502+
503+
@_util.copy_func_kwargs(_options.AuthOptions)
504+
def on_user_created(
505+
**kwargs,
506+
) -> _typing.Callable[[AuthEventCallable], AuthEventCallable]:
507+
"""
508+
Handles an event that is triggered when a user is created in Firebase Authentication.
509+
510+
Example:
511+
512+
.. code-block:: python
513+
514+
from firebase_functions import identity_fn
515+
516+
@identity_fn.on_user_created()
517+
def example(event: identity_fn.AuthEvent) -> None:
518+
pass
519+
520+
:param \\*\\*kwargs: Options.
521+
:type \\*\\*kwargs: as :exc:`firebase_functions.options.AuthOptions`
522+
:rtype: :exc:`typing.Callable`
523+
\\[ \\[ :exc:`firebase_functions.identity_fn.AuthEvent` \\], `None` \\]
524+
A function that takes an AuthEvent and returns ``None``.
525+
"""
526+
options = _options.AuthOptions(**kwargs)
527+
528+
def on_user_created_decorator(func: AuthEventCallable):
529+
from firebase_functions.private._identity_fn import (
530+
event_type_user_created,
531+
user_event_handler,
532+
)
533+
534+
@_functools.wraps(func)
535+
def on_user_created_wrapped(raw: _ce.CloudEvent):
536+
return user_event_handler(func, raw)
537+
538+
_util.set_func_endpoint_attr(
539+
on_user_created_wrapped,
540+
options._endpoint(
541+
func_name=func.__name__,
542+
event_type=event_type_user_created,
543+
),
544+
)
545+
return on_user_created_wrapped
546+
547+
return on_user_created_decorator
548+
549+
550+
@_util.copy_func_kwargs(_options.AuthOptions)
551+
def on_user_deleted(
552+
**kwargs,
553+
) -> _typing.Callable[[AuthEventCallable], AuthEventCallable]:
554+
"""
555+
Handles an event that is triggered when a user is deleted in Firebase Authentication.
556+
557+
Example:
558+
559+
.. code-block:: python
560+
561+
from firebase_functions import identity_fn
562+
563+
@identity_fn.on_user_deleted()
564+
def example(event: identity_fn.AuthEvent) -> None:
565+
pass
566+
567+
:param \\*\\*kwargs: Options.
568+
:type \\*\\*kwargs: as :exc:`firebase_functions.options.AuthOptions`
569+
:rtype: :exc:`typing.Callable`
570+
\\[ \\[ :exc:`firebase_functions.identity_fn.AuthEvent` \\], `None` \\]
571+
A function that takes an AuthEvent and returns ``None``.
572+
"""
573+
options = _options.AuthOptions(**kwargs)
574+
575+
def on_user_deleted_decorator(func: AuthEventCallable):
576+
from firebase_functions.private._identity_fn import (
577+
event_type_user_deleted,
578+
user_event_handler,
579+
)
580+
581+
@_functools.wraps(func)
582+
def on_user_deleted_wrapped(raw: _ce.CloudEvent):
583+
return user_event_handler(func, raw)
584+
585+
_util.set_func_endpoint_attr(
586+
on_user_deleted_wrapped,
587+
options._endpoint(
588+
func_name=func.__name__,
589+
event_type=event_type_user_deleted,
590+
),
591+
)
592+
return on_user_deleted_wrapped
593+
594+
return on_user_deleted_decorator

‎src/firebase_functions/options.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,38 @@ def _required_apis(self) -> list[_manifest.ManifestRequiredApi]:
10551055
]
10561056

10571057

1058+
@_dataclasses.dataclass(frozen=True, kw_only=True)
1059+
class AuthOptions(EventHandlerOptions):
1060+
"""
1061+
Options that can be set on an Auth event trigger.
1062+
Internal use only.
1063+
"""
1064+
1065+
tenant_id: str | Expression[str] | None = None
1066+
"""
1067+
An optional tenant ID to scope the trigger to.
1068+
"""
1069+
1070+
def _endpoint(
1071+
self,
1072+
**kwargs,
1073+
) -> _manifest.ManifestEndpoint:
1074+
assert kwargs["event_type"] is not None
1075+
event_filters: dict[str, str | Expression[str]] = {}
1076+
if self.tenant_id is not None:
1077+
event_filters["tenantid"] = self.tenant_id
1078+
1079+
endpoint = super()._endpoint(
1080+
**kwargs,
1081+
event_filters=event_filters,
1082+
)
1083+
assert endpoint.eventTrigger is not None
1084+
# Firebase Auth event triggers are global by nature, so Eventarc requires
1085+
# the trigger region to be "global" even when the Cloud Function is regional.
1086+
endpoint.eventTrigger["region"] = "global"
1087+
return endpoint
1088+
1089+
10581090
@_dataclasses.dataclass(frozen=True, kw_only=True)
10591091
class FirestoreOptions(RuntimeOptions):
10601092
"""

0 commit comments

Comments
 (0)