Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy3.8']
python: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy3.9']

steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ jobs:
uses: actions/[email protected]
with:
name: dist
path: dist

- name: Publish preflight check
id: preflight
Expand Down
2 changes: 1 addition & 1 deletion firebase_admin/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

"""About information (version, etc) for Firebase Admin SDK."""

__version__ = '6.5.0'
__version__ = '6.6.0'
__title__ = 'firebase_admin'
__author__ = 'Firebase'
__license__ = 'Apache License 2.0'
Expand Down
8 changes: 6 additions & 2 deletions firebase_admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import os
import threading

from google.auth.credentials import Credentials as GoogleAuthCredentials
from google.auth.exceptions import DefaultCredentialsError
from firebase_admin import credentials
from firebase_admin.__about__ import __version__
Expand Down Expand Up @@ -208,10 +209,13 @@ def __init__(self, name, credential, options):
'non-empty string.'.format(name))
self._name = name

if not isinstance(credential, credentials.Base):
if isinstance(credential, GoogleAuthCredentials):
self._credential = credentials._ExternalCredentials(credential) # pylint: disable=protected-access
elif isinstance(credential, credentials.Base):
self._credential = credential
else:
raise ValueError('Illegal Firebase credential provided. App must be initialized '
'with a valid credential instance.')
self._credential = credential
self._options = _AppOptions(options)
self._lock = threading.RLock()
self._services = {}
Expand Down
5 changes: 5 additions & 0 deletions firebase_admin/_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import requests
from requests.packages.urllib3.util import retry # pylint: disable=import-error

from firebase_admin import _utils

if hasattr(retry.Retry.DEFAULT, 'allowed_methods'):
_ANY_METHOD = {'allowed_methods': None}
Expand All @@ -36,6 +37,9 @@

DEFAULT_TIMEOUT_SECONDS = 120

METRICS_HEADERS = {
'X-GOOG-API-CLIENT': _utils.get_metrics_header(),
}

class HttpClient:
"""Base HTTP client used to make HTTP calls.
Expand Down Expand Up @@ -72,6 +76,7 @@ def __init__(

if headers:
self._session.headers.update(headers)
self._session.headers.update(METRICS_HEADERS)
if retries:
self._session.mount('http://', requests.adapters.HTTPAdapter(max_retries=retries))
self._session.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries))
Expand Down
3 changes: 3 additions & 0 deletions firebase_admin/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""Internal utilities common to all modules."""

import json
from platform import python_version

import google.auth
import requests
Expand Down Expand Up @@ -75,6 +76,8 @@
16: exceptions.UNAUTHENTICATED,
}

def get_metrics_header():
return f'gl-python/{python_version()} fire-admin/{firebase_admin.__version__}'

def _get_initialized_app(app):
"""Returns a reference to an initialized App instance."""
Expand Down
7 changes: 6 additions & 1 deletion firebase_admin/app_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ class _AppCheckService:
_scoped_project_id = None
_jwks_client = None

_APP_CHECK_HEADERS = {
'X-GOOG-API-CLIENT': _utils.get_metrics_header(),
}

def __init__(self, app):
# Validate and store the project_id to validate the JWT claims
self._project_id = app.project_id
Expand All @@ -62,7 +66,8 @@ def __init__(self, app):
'GOOGLE_CLOUD_PROJECT environment variable.')
self._scoped_project_id = 'projects/' + app.project_id
# Default lifespan is 300 seconds (5 minutes) so we change it to 21600 seconds (6 hours).
self._jwks_client = PyJWKClient(self._JWKS_URL, lifespan=21600)
self._jwks_client = PyJWKClient(
self._JWKS_URL, lifespan=21600, headers=self._APP_CHECK_HEADERS)


def verify_token(self, token: str) -> Dict[str, Any]:
Expand Down
14 changes: 14 additions & 0 deletions firebase_admin/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import pathlib

import google.auth
from google.auth.credentials import Credentials as GoogleAuthCredentials
from google.auth.transport import requests
from google.oauth2 import credentials
from google.oauth2 import service_account
Expand Down Expand Up @@ -58,6 +59,19 @@ def get_credential(self):
"""Returns the Google credential instance used for authentication."""
raise NotImplementedError

class _ExternalCredentials(Base):
"""A wrapper for google.auth.credentials.Credentials typed credential instances"""

def __init__(self, credential: GoogleAuthCredentials):
super(_ExternalCredentials, self).__init__()
self._g_credential = credential

def get_credential(self):
"""Returns the underlying Google Credential

Returns:
google.auth.credentials.Credentials: A Google Auth credential instance."""
return self._g_credential

class Certificate(Base):
"""A credential initialized from a JSON certificate keyfile."""
Expand Down
88 changes: 52 additions & 36 deletions firebase_admin/firestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,59 +18,75 @@
Firebase apps. This requires the ``google-cloud-firestore`` Python module.
"""

from __future__ import annotations
from typing import Optional, Dict
from firebase_admin import App
from firebase_admin import _utils

try:
from google.cloud import firestore # pylint: disable=import-error,no-name-in-module
from google.cloud import firestore
from google.cloud.firestore_v1.base_client import DEFAULT_DATABASE
existing = globals().keys()
for key, value in firestore.__dict__.items():
if not key.startswith('_') and key not in existing:
globals()[key] = value
except ImportError:
except ImportError as error:
raise ImportError('Failed to import the Cloud Firestore library for Python. Make sure '
'to install the "google-cloud-firestore" module.')

from firebase_admin import _utils
'to install the "google-cloud-firestore" module.') from error


_FIRESTORE_ATTRIBUTE = '_firestore'


def client(app=None) -> firestore.Client:
def client(app: Optional[App] = None, database_id: Optional[str] = None) -> firestore.Client:
"""Returns a client that can be used to interact with Google Cloud Firestore.

Args:
app: An App instance (optional).
app: An App instance (optional).
database_id: The database ID of the Google Cloud Firestore database to be used.
Defaults to the default Firestore database ID if not specified or an empty string
(optional).

Returns:
google.cloud.firestore.Firestore: A `Firestore Client`_.
google.cloud.firestore.Firestore: A `Firestore Client`_.

Raises:
ValueError: If a project ID is not specified either via options, credentials or
environment variables, or if the specified project ID is not a valid string.
ValueError: If the specified database ID is not a valid string, or if a project ID is not
specified either via options, credentials or environment variables, or if the specified
project ID is not a valid string.

.. _Firestore Client: https://googlecloudplatform.github.io/google-cloud-python/latest\
/firestore/client.html
.. _Firestore Client: https://cloud.google.com/python/docs/reference/firestore/latest/\
google.cloud.firestore_v1.client.Client
"""
fs_client = _utils.get_app_service(app, _FIRESTORE_ATTRIBUTE, _FirestoreClient.from_app)
return fs_client.get()


class _FirestoreClient:
"""Holds a Google Cloud Firestore client instance."""

def __init__(self, credentials, project):
self._client = firestore.Client(credentials=credentials, project=project)

def get(self):
return self._client

@classmethod
def from_app(cls, app):
"""Creates a new _FirestoreClient for the specified app."""
credentials = app.credential.get_credential()
project = app.project_id
if not project:
raise ValueError(
'Project ID is required to access Firestore. Either set the projectId option, '
'or use service account credentials. Alternatively, set the GOOGLE_CLOUD_PROJECT '
'environment variable.')
return _FirestoreClient(credentials, project)
# Validate database_id
if database_id is not None and not isinstance(database_id, str):
raise ValueError(f'database_id "{database_id}" must be a string or None.')
fs_service = _utils.get_app_service(app, _FIRESTORE_ATTRIBUTE, _FirestoreService)
return fs_service.get_client(database_id)


class _FirestoreService:
"""Service that maintains a collection of firestore clients."""

def __init__(self, app: App) -> None:
self._app: App = app
self._clients: Dict[str, firestore.Client] = {}

def get_client(self, database_id: Optional[str]) -> firestore.Client:
"""Creates a client based on the database_id. These clients are cached."""
database_id = database_id or DEFAULT_DATABASE
if database_id not in self._clients:
# Create a new client and cache it in _clients
credentials = self._app.credential.get_credential()
project = self._app.project_id
if not project:
raise ValueError(
'Project ID is required to access Firestore. Either set the projectId option, '
'or use service account credentials. Alternatively, set the '
'GOOGLE_CLOUD_PROJECT environment variable.')

fs_client = firestore.Client(
credentials=credentials, project=project, database=database_id)
self._clients[database_id] = fs_client

return self._clients[database_id]
94 changes: 52 additions & 42 deletions firebase_admin/firestore_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,65 +18,75 @@
associated with Firebase apps. This requires the ``google-cloud-firestore`` Python module.
"""

from typing import Type

from firebase_admin import (
App,
_utils,
)
from firebase_admin.credentials import Base
from __future__ import annotations
from typing import Optional, Dict
from firebase_admin import App
from firebase_admin import _utils

try:
from google.cloud import firestore # type: ignore # pylint: disable=import-error,no-name-in-module
from google.cloud import firestore
from google.cloud.firestore_v1.base_client import DEFAULT_DATABASE
existing = globals().keys()
for key, value in firestore.__dict__.items():
if not key.startswith('_') and key not in existing:
globals()[key] = value
except ImportError:
except ImportError as error:
raise ImportError('Failed to import the Cloud Firestore library for Python. Make sure '
'to install the "google-cloud-firestore" module.')
'to install the "google-cloud-firestore" module.') from error


_FIRESTORE_ASYNC_ATTRIBUTE: str = '_firestore_async'


def client(app: App = None) -> firestore.AsyncClient:
def client(app: Optional[App] = None, database_id: Optional[str] = None) -> firestore.AsyncClient:
"""Returns an async client that can be used to interact with Google Cloud Firestore.

Args:
app: An App instance (optional).
app: An App instance (optional).
database_id: The database ID of the Google Cloud Firestore database to be used.
Defaults to the default Firestore database ID if not specified or an empty string
(optional).

Returns:
google.cloud.firestore.Firestore_Async: A `Firestore Async Client`_.
google.cloud.firestore.Firestore_Async: A `Firestore Async Client`_.

Raises:
ValueError: If a project ID is not specified either via options, credentials or
environment variables, or if the specified project ID is not a valid string.
ValueError: If the specified database ID is not a valid string, or if a project ID is not
specified either via options, credentials or environment variables, or if the specified
project ID is not a valid string.

.. _Firestore Async Client: https://googleapis.dev/python/firestore/latest/client.html
.. _Firestore Async Client: https://cloud.google.com/python/docs/reference/firestore/latest/\
google.cloud.firestore_v1.async_client.AsyncClient
"""
fs_client = _utils.get_app_service(
app, _FIRESTORE_ASYNC_ATTRIBUTE, _FirestoreAsyncClient.from_app)
return fs_client.get()


class _FirestoreAsyncClient:
"""Holds a Google Cloud Firestore Async Client instance."""

def __init__(self, credentials: Type[Base], project: str) -> None:
self._client = firestore.AsyncClient(credentials=credentials, project=project)

def get(self) -> firestore.AsyncClient:
return self._client

@classmethod
def from_app(cls, app: App) -> "_FirestoreAsyncClient":
# Replace remove future reference quotes by importing annotations in Python 3.7+ b/238779406
"""Creates a new _FirestoreAsyncClient for the specified app."""
credentials = app.credential.get_credential()
project = app.project_id
if not project:
raise ValueError(
'Project ID is required to access Firestore. Either set the projectId option, '
'or use service account credentials. Alternatively, set the GOOGLE_CLOUD_PROJECT '
'environment variable.')
return _FirestoreAsyncClient(credentials, project)
# Validate database_id
if database_id is not None and not isinstance(database_id, str):
raise ValueError(f'database_id "{database_id}" must be a string or None.')

fs_service = _utils.get_app_service(app, _FIRESTORE_ASYNC_ATTRIBUTE, _FirestoreAsyncService)
return fs_service.get_client(database_id)

class _FirestoreAsyncService:
"""Service that maintains a collection of firestore async clients."""

def __init__(self, app: App) -> None:
self._app: App = app
self._clients: Dict[str, firestore.AsyncClient] = {}

def get_client(self, database_id: Optional[str]) -> firestore.AsyncClient:
"""Creates an async client based on the database_id. These clients are cached."""
database_id = database_id or DEFAULT_DATABASE
if database_id not in self._clients:
# Create a new client and cache it in _clients
credentials = self._app.credential.get_credential()
project = self._app.project_id
if not project:
raise ValueError(
'Project ID is required to access Firestore. Either set the projectId option, '
'or use service account credentials. Alternatively, set the '
'GOOGLE_CLOUD_PROJECT environment variable.')

fs_client = firestore.AsyncClient(
credentials=credentials, project=project, database=database_id)
self._clients[database_id] = fs_client

return self._clients[database_id]
Loading