Skip to content
Open
124 changes: 122 additions & 2 deletions src/firebase_functions/identity_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Cloud functions to handle Eventarc events."""
"""Cloud functions to handle Firebase Authentication and Identity Platform events."""

# pylint: disable=protected-access,cyclic-import
import dataclasses as _dataclasses
Expand All @@ -20,15 +20,18 @@
import typing as _typing
from enum import Enum

import cloudevents.http as _ce
from flask import (
Request as _Request,
)
from flask import (
Response as _Response,
)

import firebase_functions.core as _core
import firebase_functions.options as _options
import firebase_functions.private.util as _util
from firebase_functions.options import AuthOptions # noqa: F401

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Since this package ships py.typed (PEP 561), strict type checkers (mypy --no-implicit-reexport, pyright) treat from ... import AuthOptions as private unless it uses as AuthOptions (or __all__). Using as AuthOptions also removes the need for # noqa: F401.

Suggested change
from firebase_functions.options import AuthOptions # noqa: F401
from firebase_functions.options import AuthOptions as AuthOptions



@_dataclasses.dataclass(frozen=True)
Expand Down Expand Up @@ -116,7 +119,7 @@ class AuthMultiFactorSettings:
@_dataclasses.dataclass(frozen=True)
class AuthUserRecord:
"""
The UserRecord passed to auth blocking functions from the identity platform.
The UserRecord passed to auth blocking and event functions from the identity platform.
"""

uid: str
Expand Down Expand Up @@ -472,3 +475,120 @@ def before_user_created_wrapped(request: _Request) -> _Response:
return before_user_created_wrapped

return before_user_created_decorator


@_dataclasses.dataclass(frozen=True)
class AuthEvent(_core.CloudEvent[AuthUserRecord]):
"""
A CloudEvent that contains an AuthUserRecord when a user is created or deleted.
"""

project: str | None = None
"""
The project identifier.
"""

tenant_id: str | None = None
"""
The ID of the Identity Platform tenant associated with the event, if applicable.
"""


AuthEventCallable = _typing.Callable[[AuthEvent], None]
"""
The type of the callable for 'on_user_created' and 'on_user_deleted' Auth events.
"""


@_util.copy_func_kwargs(_options.AuthOptions)
def on_user_created(
**kwargs,
) -> _typing.Callable[[AuthEventCallable], AuthEventCallable]:
"""
Handles an event that is triggered when a user is created in Firebase Authentication.

Example:

.. code-block:: python

from firebase_functions import identity_fn

@identity_fn.on_user_created()
def example(event: identity_fn.AuthEvent) -> None:
pass

:param \\*\\*kwargs: Options.
:type \\*\\*kwargs: as :exc:`firebase_functions.options.AuthOptions`
:rtype: :exc:`typing.Callable`
\\[ \\[ :exc:`firebase_functions.identity_fn.AuthEvent` \\], `None` \\]
A function that takes an AuthEvent and returns ``None``.
"""
options = _options.AuthOptions(**kwargs)

def on_user_created_decorator(func: AuthEventCallable):
from firebase_functions.private._identity_fn import (
event_type_user_created,
user_event_handler,
)

@_functools.wraps(func)
def on_user_created_wrapped(raw: _ce.CloudEvent):
return user_event_handler(func, raw)

_util.set_func_endpoint_attr(
on_user_created_wrapped,
options._endpoint(
func_name=func.__name__,
event_type=event_type_user_created,
),
)
return on_user_created_wrapped

return on_user_created_decorator


@_util.copy_func_kwargs(_options.AuthOptions)
def on_user_deleted(
**kwargs,
) -> _typing.Callable[[AuthEventCallable], AuthEventCallable]:
"""
Handles an event that is triggered when a user is deleted in Firebase Authentication.

Example:

.. code-block:: python

from firebase_functions import identity_fn

@identity_fn.on_user_deleted()
def example(event: identity_fn.AuthEvent) -> None:
pass

:param \\*\\*kwargs: Options.
:type \\*\\*kwargs: as :exc:`firebase_functions.options.AuthOptions`
:rtype: :exc:`typing.Callable`
\\[ \\[ :exc:`firebase_functions.identity_fn.AuthEvent` \\], `None` \\]
A function that takes an AuthEvent and returns ``None``.
"""
options = _options.AuthOptions(**kwargs)

def on_user_deleted_decorator(func: AuthEventCallable):
from firebase_functions.private._identity_fn import (
event_type_user_deleted,
user_event_handler,
)

@_functools.wraps(func)
def on_user_deleted_wrapped(raw: _ce.CloudEvent):
return user_event_handler(func, raw)

_util.set_func_endpoint_attr(
on_user_deleted_wrapped,
options._endpoint(
func_name=func.__name__,
event_type=event_type_user_deleted,
),
)
return on_user_deleted_wrapped

return on_user_deleted_decorator
32 changes: 32 additions & 0 deletions src/firebase_functions/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,38 @@ def _required_apis(self) -> list[_manifest.ManifestRequiredApi]:
]


@_dataclasses.dataclass(frozen=True, kw_only=True)
class AuthOptions(EventHandlerOptions):
"""
Options that can be set on an Auth event trigger.
Internal use only.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: AuthOptions is publicly re-exported in identity_fn.py (from firebase_functions.options import AuthOptions) and linked in the public :type **kwargs: docstrings of on_user_created and on_user_deleted. Should the "Internal use only" string be dropped?

"""

tenant_id: str | Expression[str] | None = None
"""
An optional tenant ID to scope the trigger to.
"""

def _endpoint(
self,
**kwargs,
) -> _manifest.ManifestEndpoint:
assert kwargs["event_type"] is not None
event_filters: dict[str, str | Expression[str]] = {}
if self.tenant_id is not None:
event_filters["tenantid"] = self.tenant_id

endpoint = super()._endpoint(
**kwargs,
event_filters=event_filters,
)
assert endpoint.eventTrigger is not None
# Firebase Auth event triggers are global by nature, so Eventarc requires
# the trigger region to be "global" even when the Cloud Function is regional.
endpoint.eventTrigger["region"] = "global"
return endpoint


@_dataclasses.dataclass(frozen=True, kw_only=True)
class FirestoreOptions(RuntimeOptions):
"""
Expand Down
Loading
Loading