Repository navigation
feat(identity): support IS_NOT_TENANT filtering and emulator timestamps - #319
shettyvarun268 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for filtering Auth event triggers to only trigger on users in the default project (no tenant) by setting tenant_id to IS_NOT_TENANT. Since Eventarc does not support empty-string filters, tenant-scoped events are filtered out at runtime. Additionally, it adds support for parsing RFC-2822 timestamps emitted by the Firebase Auth Emulator. Feedback on the changes suggests ensuring that all parsed or provided datetime objects in _parse_auth_timestamp are normalized to timezone-aware UTC datetimes to prevent returning naive datetimes or datetimes with non-UTC offsets.
| def _parse_auth_timestamp(value: _typing.Any) -> _dt.datetime | None: | ||
| """Normalizes Auth event timestamps (epoch ms numbers/strings or ISO-8601 strings) to UTC.""" | ||
| """ | ||
| Normalizes Auth event timestamps to UTC datetimes. | ||
| Supports epoch milliseconds (int/float/numeric str), ISO-8601 strings (production | ||
| Eventarc), and RFC-2822 strings (emitted by the Firebase Auth Emulator). | ||
| """ | ||
| if isinstance(value, _dt.datetime): | ||
| return value | ||
| if isinstance(value, int | float) and not isinstance(value, bool): | ||
| return _dt.datetime.fromtimestamp(value / 1000.0, tz=_dt.timezone.utc) | ||
| if isinstance(value, str) and value: | ||
| if value.isdigit(): | ||
| return _dt.datetime.fromtimestamp(int(value) / 1000.0, tz=_dt.timezone.utc) | ||
| for parser in (_util.timestamp_conversion, _dt.datetime.fromisoformat): | ||
| for parser in ( | ||
| _util.timestamp_conversion, | ||
| _dt.datetime.fromisoformat, | ||
| _email_utils.parsedate_to_datetime, | ||
| ): | ||
| try: | ||
| return parser(value) | ||
| except ValueError: | ||
| except (ValueError, TypeError): | ||
| continue | ||
| return None |
There was a problem hiding this comment.
To ensure consistency and correctness across all parsing paths, any parsed or provided datetime object should be normalized to a timezone-aware UTC datetime. Currently, _email_utils.parsedate_to_datetime or _dt.datetime.fromisoformat can return naive datetimes or datetimes with non-UTC offsets. Normalizing them to UTC guarantees that the returned value always matches the docstring's contract.
def _parse_auth_timestamp(value: _typing.Any) -> _dt.datetime | None:
"""
Normalizes Auth event timestamps to UTC datetimes.
Supports epoch milliseconds (int/float/numeric str), ISO-8601 strings (production
Eventarc), and RFC-2822 strings (emitted by the Firebase Auth Emulator).
"""
if isinstance(value, _dt.datetime):
if value.tzinfo is None:
return value.replace(tzinfo=_dt.timezone.utc)
return value.astimezone(_dt.timezone.utc)
if isinstance(value, int | float) and not isinstance(value, bool):
return _dt.datetime.fromtimestamp(value / 1000.0, tz=_dt.timezone.utc)
if isinstance(value, str) and value:
if value.isdigit():
return _dt.datetime.fromtimestamp(int(value) / 1000.0, tz=_dt.timezone.utc)
for parser in (
_util.timestamp_conversion,
_dt.datetime.fromisoformat,
_email_utils.parsedate_to_datetime,
):
try:
dt = parser(value)
if dt.tzinfo is None:
return dt.replace(tzinfo=_dt.timezone.utc)
return dt.astimezone(_dt.timezone.utc)
except (ValueError, TypeError):
continue
return None552a64a to
caefc5b
Compare
425136d to
87cb6b6
Compare
87cb6b6 to
44bbe64
Compare
Summary
IS_NOT_TENANTsupport toAuthOptions.tenant_idso developers can scopeon_user_createdandon_user_deletedtriggers exclusively to default-project (non-tenant) users, matching the Node.js SDK (firebase-functions/v2/identity).email.utils.parsedate_to_datetime) in_parse_auth_timestampto support user metadata timestamps emitted by the local Firebase Auth Emulator.Usage
Architectural & Implementation Details
1. Why
IS_NOT_TENANTUses Hybrid Deploy-Time + Runtime Filtering (options.py,identity_fn.py,private/_identity_fn.py)In Identity Platform multi-tenancy, there are three ways a developer may configure
tenant_idon an Auth trigger:tenant_id=None(default): Trigger for all users across the project and all tenants (eventFilters: {}).tenant_id="tenant-a": Trigger only for users in"tenant-a"(eventFilters: {"tenantid": "tenant-a"}).tenant_id=IS_NOT_TENANT: Trigger only for default-project users who do not belong to any tenant.Platform Constraint: Google Cloud Eventarc does not support negative filters or empty-string filters (
eventFilters: {"tenantid": ""}is rejected by the Eventarc API). To supportIS_NOT_TENANT(matching the Node SDK's implementation):IS_NOT_TENANT = _util.Sentinel(...)instance infirebase_functions.optionsand re-exported it infirebase_functions.identity_fn.AuthOptions._endpoint): Whenself.tenant_idis a_util.Sentinel(IS_NOT_TENANT), we omit"tenantid"fromevent_filtersinfunctions.yamlso Eventarc delivers all Auth events to the function.user_event_handler): After unpacking theAuthEvent, ifoptions.tenant_id == _options.IS_NOT_TENANT and event.tenant_id is not None, the handler returns early and drops the tenant-scoped event before invoking@core.initor the user's function.2. Firebase Auth Emulator RFC-2822 Timestamp Support (
private/_identity_fn.py)metadata.createTimeandmetadata.lastSignInTimeas ISO-8601 strings ("2023-01-01T00:00:00Z"), the local Firebase Auth Emulator serializesmetadata.creationTimeandmetadata.lastSignInTimeusing JavaScript'sDate.prototype.toUTCString(), which produces RFC-2822 strings (e.g.,"Sun, 01 Jan 2023 00:00:00 GMT").email.utils.parsedate_to_datetimeas a fallback parser in_parse_auth_timestamp(normalized to UTC via_ensure_utc). This ensuresevent.data.metadata.creation_timeandlast_sign_in_timeare properly parsed into timezone-aware UTCdatetimeobjects when testing locally with the Firebase Emulator Suite.Verification
on_user_created(tenant_id=IS_NOT_TENANT)on_user_created(tenant_id="tenant-a")on_user_created()(tenant_id=None)tenant_id=None)tenant_id="tenant-a")tenant_id="tenant-b")pytest(all 147 tests passing, includingtest_on_user_created_is_not_tenantandtest_auth_emulator_rfc2822_timestamps)mypy,ruff check,ruff format --check, and./docs/generate.shfirebase emulators:exec).on_user_created/on_user_deletedIS_NOT_TENANT, specifictenant_id, andtenant_id=None).PR Stack: