From f1106724eb7faf1d276d21a8492f9c681a8ead84 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Mon, 10 Jun 2024 13:52:48 -0600 Subject: [PATCH 1/9] Use google default auth mechanism to allow for more options --- README.md | 4 ++-- scripts-dev/proxy-test/sygnal.yaml | 1 - sygnal.yaml.sample | 1 - sygnal/gcmpushkin.py | 36 +++++++++++++----------------- tests/test_gcm.py | 32 +++++--------------------- 5 files changed, 23 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index d6e9efcf..0f5ba0a6 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,8 @@ For API v1, it expects: - the `project_id` parameter to contain the `Project ID`, which can be acquired from Firebase Console at: `https://console.cloud.google.com/project//settings/general/` -- the `service_account_file` parameter to contain the path to the service account file, - which can be acquired from Firebase Console at: +- the [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) configured on the local system. + This can point to a service account file which can be acquired from Firebase Console at: `https://console.firebase.google.com/project//settings/serviceaccounts/adminsdk` Using an HTTP Proxy for outbound traffic diff --git a/scripts-dev/proxy-test/sygnal.yaml b/scripts-dev/proxy-test/sygnal.yaml index 4d91d446..09406c21 100644 --- a/scripts-dev/proxy-test/sygnal.yaml +++ b/scripts-dev/proxy-test/sygnal.yaml @@ -63,4 +63,3 @@ apps: type: gcm api_version: v1 project_id: - service_account_file: /service_account.json diff --git a/sygnal.yaml.sample b/sygnal.yaml.sample index 1b59d4c5..6c50da80 100644 --- a/sygnal.yaml.sample +++ b/sygnal.yaml.sample @@ -210,7 +210,6 @@ apps: # #api_key: # api_version: v1 # project_id: project-id - # service_account_file: /path/to/service_account.json # # # This is the maximum number of connections to GCM servers at any one time # # the default is 20. diff --git a/sygnal/gcmpushkin.py b/sygnal/gcmpushkin.py index fa3360ec..9388fadb 100644 --- a/sygnal/gcmpushkin.py +++ b/sygnal/gcmpushkin.py @@ -27,7 +27,7 @@ # https://github.com/googleapis/google-auth-library-python/issues/613 import aiohttp import google.auth.transport._aiohttp_requests -from google.auth._default_async import load_credentials_from_file +from google.auth._default_async import default_async from google.oauth2._credentials_async import Credentials from opentracing import Span, logs, tags from prometheus_client import Counter, Gauge, Histogram @@ -126,7 +126,6 @@ class GcmPushkin(ConcurrencyLimitedPushkin): "fcm_options", "max_connections", "project_id", - "service_account_file", } | ConcurrencyLimitedPushkin.UNDERSTOOD_CONFIG_FIELDS def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]) -> None: @@ -186,22 +185,26 @@ def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]) -> None: "Must configure `project_id` when using FCM api v1", ) + self._load_credentials(proxy_url) + + # Use the fcm_options config dictionary as a foundation for the body; + # this lets the Sygnal admin choose custom FCM options + # (e.g. content_available). + self.base_request_body = self.get_config("fcm_options", dict, {}) + if not isinstance(self.base_request_body, dict): + raise PushkinSetupException( + "Config field fcm_options, if set, must be a dictionary of options" + ) + + def _load_credentials(self, proxy_url: str | None) -> None: self.credentials: Optional[Credentials] = None if self.api_version is APIVersion.V1: - self.service_account_file = self.get_config("service_account_file", str) - if not self.service_account_file: - raise PushkinSetupException( - "Must configure `service_account_file` when using FCM api v1", - ) try: - self.credentials, _ = load_credentials_from_file( - str(self.service_account_file), - scopes=AUTH_SCOPES, - ) + self.credentials, _ = default_async(scopes=AUTH_SCOPES) except google.auth.exceptions.DefaultCredentialsError as e: raise PushkinSetupException( - f"`service_account_file` must be valid: {str(e)}", + f"Failed loading google credentials: {str(e)}", ) session = None @@ -215,15 +218,6 @@ def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]) -> None: session=session ) - # Use the fcm_options config dictionary as a foundation for the body; - # this lets the Sygnal admin choose custom FCM options - # (e.g. content_available). - self.base_request_body = self.get_config("fcm_options", dict, {}) - if not isinstance(self.base_request_body, dict): - raise PushkinSetupException( - "Config field fcm_options, if set, must be a dictionary of options" - ) - @classmethod async def create( cls, name: str, sygnal: "Sygnal", config: Dict[str, Any] diff --git a/tests/test_gcm.py b/tests/test_gcm.py index aa35f015..d49381d3 100644 --- a/tests/test_gcm.py +++ b/tests/test_gcm.py @@ -108,8 +108,6 @@ def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]): self.last_request_body: Dict[str, Any] = {} self.last_request_headers: Dict[AnyStr, List[AnyStr]] = {} # type: ignore[valid-type] self.num_requests = 0 - if self.api_version is APIVersion.V1: - self.credentials = TestCredentials() # type: ignore[assignment] def preload_with_response( self, code: int, response_payload: Dict[str, Any] @@ -120,6 +118,12 @@ def preload_with_response( self.preloaded_response = DummyResponse(code) self.preloaded_response_payload = response_payload + def _load_credentials(self, proxy_url: str | None) -> None: + if self.api_version is APIVersion.V1: + self.credentials = TestCredentials() # type: ignore[assignment] + self.google_auth_request = None # type: ignore[assignment] + pass + async def _perform_http_request( # type: ignore[override] self, body: Dict[str, Any], headers: Dict[AnyStr, List[AnyStr]] ) -> Tuple[DummyResponse, str]: @@ -134,23 +138,6 @@ async def _refresh_credentials(self) -> None: await self.credentials.refresh(self.google_auth_request) -FAKE_SERVICE_ACCOUNT_FILE = b""" -{ - "type": "service_account", - "project_id": "project_id", - "private_key_id": "private_key_id", - "private_key": "-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0PwE6TeTHjD5R\\nY2nOw1rsTgQZ38LCR2CLtx36n+LUkgej/9b+fwC88oKIqJKjUwn43JEOhf4rbA/a\\nqo4jVoLgv754G5+7Glfarr3/rqg+AVT75x6J5DRvhIYpDXwMIUqLAAbfk3TTFNJn\\n2ctrkBF2ZP9p3mzZ3NRjU63Wbf3LBpRqs8jdFEQu8JAecG8VKV1mboJIXG3hwqFN\\nJmcpC/+sWaxB5iMgSqy0w/rGFs6ZbZF6D10XYvf40lEEk9jQIovT+QD4+6GTlroT\\nbOk8uIwxFQcwMFpXj4MktqVNSNyiuuttptIvBWcMWHlaabXrR89vqUFe1g1Jx4GL\\nCF89RrcLAgMBAAECggEAPUYZ3b8zId78JGDeTEq+8wwGeuFFbRQkrvpeN5/41Xib\\nHlZPuQ5lqtXqKBjeWKVXA4G/0icc45gFv7kxPrQfI9YrItuJLmrjKNU0g+HVEdcU\\nE9pa2Fd6t9peXUBXRixfEee9bm3LTiKK8IDqlTNRrGTjKxNQ/7MBhI6izv1vRH/x\\n8i0o1xxNdqstHZ9wBFKYO9w8UQjtfzckkBNDLkaJ/WN0BoRubmUiV1+KwAyyBr6O\\nRnnZ9Tvy8VraSNSdJhX36ai36y18/sT6PWOp99zHYuDyz89KIz1la/fT9eSoR0Jy\\nYePmTEi+9pWhvtpAkqJkRxe5IDz71JVsQ07KoVfzaQKBgQDzKKUd/0ujhv/B9MQf\\nHcwSeWu/XnQ4hlcwz8dTWQjBV8gv9l4yBj9Pra62rg/tQ7b5XKMt6lv/tWs1IpdA\\neMsySY4972VPrmggKXgCnyKckDUYydNtHAIj9buo6AV8rONaneYnGv5wpSsf3q2c\\nOZrkamRgbBkI+B2mZ2obH1oVlQKBgQC9w9HkrDMvZ5L/ilZmpsvoHNFlQwmDgNlN\\n0ej5QGID5rljRM3CcLNHdyQiKqvLA9MCpPEXb2vVJPdmquD12A7a9s0OwxB/dtOD\\nykofcTY0ZHEM1HEyYJGmdK4FvZuNU4o2/D268dePjtj1Xw3c5fs0bcDiGQMtjWlz\\n5hjBzMsyHwKBgGjrIsPcwlBfEcAo0u7yNnnKNnmuUcuJ+9kt7j3Cbwqty80WKvK+\\ny1agBIECfhDMZQkXtbk8JFIjf4y/zi+db1/VaTDEORy2jmtCOWw4KgEQIDj/7OBp\\nc2r8vupUovl2x+rzsrkw5pTIT+FCffqoyHLCjWkle2/pTzHb8Waekoo5AoGAbELk\\nYy5uwTO45Hr60fOEzzZpq/iz28dNshz4agL2KD2gNGcTcEO1tCbfgXKQsfDLmG2b\\ncgBKJ77AOl1wnDEYQIme8TYOGnojL8Pfx9Jh10AaUvR8Y/49+hYFFhdXQCiR6M69\\nNQM2NJuNYWdKVGUMjJu0+AjHDFzp9YonQ6Ffp4cCgYEAmVALALCjU9GjJymgJ0lx\\nD9LccVHMwf9NmR/sMg0XNePRbCEcMDHKdtVJ1zPGS5txuxY3sRb/tDpv7TfuitrU\\nAw0/2ooMzunaoF/HXo+C/+t+pfuqPqLK4sCCyezUlMfCcaPdwXN2FmbgsaFHfe7I\\n7sGEnS/d8wEgydMiptJEf9s=\\n-----END PRIVATE KEY-----\\n", - "client_email": "firebase-adminsdk@project_id.iam.gserviceaccount.com", - "client_id": "client_id", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk%40project_id.iam.gserviceaccount.com", - "universe_domain": "googleapis.com" -} -""" - - class GcmTestCase(testutils.TestCase): maxDiff = None @@ -165,14 +152,10 @@ def config_setup(self, config: Dict[str, Any]) -> None: "api_key": "kii", "fcm_options": {"content_available": True, "mutable_content": True}, } - self.service_account_file = tempfile.NamedTemporaryFile() - self.service_account_file.write(FAKE_SERVICE_ACCOUNT_FILE) - self.service_account_file.flush() config["apps"]["com.example.gcm.apiv1"] = { "type": "tests.test_gcm.TestGcmPushkin", "api_version": "v1", "project_id": "example_project", - "service_account_file": self.service_account_file.name, "fcm_options": { "android": { "notification": { @@ -193,9 +176,6 @@ def config_setup(self, config: Dict[str, Any]) -> None: }, } - def tearDown(self) -> None: - self.service_account_file.close() - def get_test_pushkin(self, name: str) -> TestGcmPushkin: pushkin = self.sygnal.pushkins[name] assert isinstance(pushkin, TestGcmPushkin) From a09aaf58691a80d9ace1e9e980e472112a710c35 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Mon, 10 Jun 2024 13:59:30 -0600 Subject: [PATCH 2/9] Update proxy-test script for new auth mechanism --- scripts-dev/proxy-test/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts-dev/proxy-test/docker-compose.yml b/scripts-dev/proxy-test/docker-compose.yml index b83f242b..2963c20d 100644 --- a/scripts-dev/proxy-test/docker-compose.yml +++ b/scripts-dev/proxy-test/docker-compose.yml @@ -5,6 +5,8 @@ services: no-internet: ipv4_address: 172.28.0.2 container_name: sygnal + environment: + GOOGLE_APPLICATION_CREDENTIALS: "/service_account.json" volumes: - ./sygnal.yaml:/sygnal.yaml - ./service_account.json:/service_account.json:ro From affaf4e60e6bf02d35fda165337be610940503e8 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Mon, 10 Jun 2024 14:08:09 -0600 Subject: [PATCH 3/9] Remove unused import --- tests/test_gcm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_gcm.py b/tests/test_gcm.py index d49381d3..2ddc22c9 100644 --- a/tests/test_gcm.py +++ b/tests/test_gcm.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import json -import tempfile from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Tuple from unittest.mock import MagicMock From ad6fbe1eaa1d76751b48de440d3a27f0df947458 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Mon, 10 Jun 2024 14:15:04 -0600 Subject: [PATCH 4/9] Add changelog entry --- changelog.d/383.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/383.feature diff --git a/changelog.d/383.feature b/changelog.d/383.feature new file mode 100644 index 00000000..6ad3b822 --- /dev/null +++ b/changelog.d/383.feature @@ -0,0 +1 @@ +Switch to google application-default-credentials for firebase auth. From a4797c38b142c95d513d59a67bfacaaa42a208c6 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Mon, 10 Jun 2024 14:24:53 -0600 Subject: [PATCH 5/9] Use Optional for param --- sygnal/gcmpushkin.py | 2 +- tests/test_gcm.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sygnal/gcmpushkin.py b/sygnal/gcmpushkin.py index 9388fadb..bbd97334 100644 --- a/sygnal/gcmpushkin.py +++ b/sygnal/gcmpushkin.py @@ -196,7 +196,7 @@ def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]) -> None: "Config field fcm_options, if set, must be a dictionary of options" ) - def _load_credentials(self, proxy_url: str | None) -> None: + def _load_credentials(self, proxy_url: Optional[str]) -> None: self.credentials: Optional[Credentials] = None if self.api_version is APIVersion.V1: diff --git a/tests/test_gcm.py b/tests/test_gcm.py index 2ddc22c9..294d9b48 100644 --- a/tests/test_gcm.py +++ b/tests/test_gcm.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import json -from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Tuple +from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Optional, Tuple from unittest.mock import MagicMock from sygnal.gcmpushkin import APIVersion, GcmPushkin @@ -117,7 +117,7 @@ def preload_with_response( self.preloaded_response = DummyResponse(code) self.preloaded_response_payload = response_payload - def _load_credentials(self, proxy_url: str | None) -> None: + def _load_credentials(self, proxy_url: Optional[str]) -> None: if self.api_version is APIVersion.V1: self.credentials = TestCredentials() # type: ignore[assignment] self.google_auth_request = None # type: ignore[assignment] From fccaa44e2cf968866b7e972fac69a95dc9ae20d0 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Mon, 10 Jun 2024 14:44:52 -0600 Subject: [PATCH 6/9] Allow use of loading credentials from file --- sygnal/gcmpushkin.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/sygnal/gcmpushkin.py b/sygnal/gcmpushkin.py index bbd97334..3f567390 100644 --- a/sygnal/gcmpushkin.py +++ b/sygnal/gcmpushkin.py @@ -27,7 +27,7 @@ # https://github.com/googleapis/google-auth-library-python/issues/613 import aiohttp import google.auth.transport._aiohttp_requests -from google.auth._default_async import default_async +from google.auth._default_async import default_async, load_credentials_from_file from google.oauth2._credentials_async import Credentials from opentracing import Span, logs, tags from prometheus_client import Counter, Gauge, Histogram @@ -126,6 +126,7 @@ class GcmPushkin(ConcurrencyLimitedPushkin): "fcm_options", "max_connections", "project_id", + "service_account_file", } | ConcurrencyLimitedPushkin.UNDERSTOOD_CONFIG_FIELDS def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]) -> None: @@ -200,12 +201,24 @@ def _load_credentials(self, proxy_url: Optional[str]) -> None: self.credentials: Optional[Credentials] = None if self.api_version is APIVersion.V1: - try: - self.credentials, _ = default_async(scopes=AUTH_SCOPES) - except google.auth.exceptions.DefaultCredentialsError as e: - raise PushkinSetupException( - f"Failed loading google credentials: {str(e)}", - ) + self.service_account_file = self.get_config("service_account_file", str) + if self.service_account_file: + try: + self.credentials, _ = load_credentials_from_file( + str(self.service_account_file), + scopes=AUTH_SCOPES, + ) + except google.auth.exceptions.DefaultCredentialsError as e: + raise PushkinSetupException( + f"`service_account_file` must be valid: {str(e)}", + ) + else: + try: + self.credentials, _ = default_async(scopes=AUTH_SCOPES) + except google.auth.exceptions.DefaultCredentialsError as e: + raise PushkinSetupException( + f"Failed loading google credentials: {str(e)}", + ) session = None if proxy_url: From 4f2d667b38ee05a48afa4bd26390121d52a9f5a3 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Mon, 10 Jun 2024 14:51:18 -0600 Subject: [PATCH 7/9] Update README to reflect both google auth methods --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 0f5ba0a6..2db50f45 100644 --- a/README.md +++ b/README.md @@ -99,10 +99,18 @@ For API v1, it expects: - the `project_id` parameter to contain the `Project ID`, which can be acquired from Firebase Console at: `https://console.cloud.google.com/project//settings/general/` + +API v1 also expects one of the following to be configured: - the [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) configured on the local system. This can point to a service account file which can be acquired from Firebase Console at: `https://console.firebase.google.com/project//settings/serviceaccounts/adminsdk` + **OR..** + +- the `service_account_file` config parameter to contain the path to the service account file + +It is recommended to use the application default credentials method to acquire google credentials. + Using an HTTP Proxy for outbound traffic ---------------------------------------- From 46e5aa3c7efabc074535ea96cf205defc5c18ed6 Mon Sep 17 00:00:00 2001 From: devonh Date: Wed, 12 Jun 2024 15:10:57 +0000 Subject: [PATCH 8/9] Update README.md Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2db50f45..522610f5 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ API v1 also expects one of the following to be configured: - the `service_account_file` config parameter to contain the path to the service account file -It is recommended to use the application default credentials method to acquire google credentials. +It is recommended to use the application default credentials method to acquire Google credentials. Using an HTTP Proxy for outbound traffic ---------------------------------------- From beb64bae165098133dff351a6de825940854915f Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Wed, 12 Jun 2024 10:50:53 -0600 Subject: [PATCH 9/9] Add test to ensure service_account loading works --- tests/test_gcm.py | 76 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/tests/test_gcm.py b/tests/test_gcm.py index 294d9b48..e856fa33 100644 --- a/tests/test_gcm.py +++ b/tests/test_gcm.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import json +import tempfile from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Optional, Tuple from unittest.mock import MagicMock @@ -117,12 +118,6 @@ def preload_with_response( self.preloaded_response = DummyResponse(code) self.preloaded_response_payload = response_payload - def _load_credentials(self, proxy_url: Optional[str]) -> None: - if self.api_version is APIVersion.V1: - self.credentials = TestCredentials() # type: ignore[assignment] - self.google_auth_request = None # type: ignore[assignment] - pass - async def _perform_http_request( # type: ignore[override] self, body: Dict[str, Any], headers: Dict[AnyStr, List[AnyStr]] ) -> Tuple[DummyResponse, str]: @@ -137,6 +132,34 @@ async def _refresh_credentials(self) -> None: await self.credentials.refresh(self.google_auth_request) +class TestGcmPushkinCredentialBypass(TestGcmPushkin): + """ + A GCM pushkin that also bypasses credential loading. + """ + + def _load_credentials(self, proxy_url: Optional[str]) -> None: + if self.api_version is APIVersion.V1: + self.credentials = TestCredentials() # type: ignore[assignment] + self.google_auth_request = None # type: ignore[assignment] + + +FAKE_SERVICE_ACCOUNT_FILE = b""" +{ + "type": "service_account", + "project_id": "test_project_id", + "private_key_id": "private_key_id", + "private_key": "-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0PwE6TeTHjD5R\\nY2nOw1rsTgQZ38LCR2CLtx36n+LUkgej/9b+fwC88oKIqJKjUwn43JEOhf4rbA/a\\nqo4jVoLgv754G5+7Glfarr3/rqg+AVT75x6J5DRvhIYpDXwMIUqLAAbfk3TTFNJn\\n2ctrkBF2ZP9p3mzZ3NRjU63Wbf3LBpRqs8jdFEQu8JAecG8VKV1mboJIXG3hwqFN\\nJmcpC/+sWaxB5iMgSqy0w/rGFs6ZbZF6D10XYvf40lEEk9jQIovT+QD4+6GTlroT\\nbOk8uIwxFQcwMFpXj4MktqVNSNyiuuttptIvBWcMWHlaabXrR89vqUFe1g1Jx4GL\\nCF89RrcLAgMBAAECggEAPUYZ3b8zId78JGDeTEq+8wwGeuFFbRQkrvpeN5/41Xib\\nHlZPuQ5lqtXqKBjeWKVXA4G/0icc45gFv7kxPrQfI9YrItuJLmrjKNU0g+HVEdcU\\nE9pa2Fd6t9peXUBXRixfEee9bm3LTiKK8IDqlTNRrGTjKxNQ/7MBhI6izv1vRH/x\\n8i0o1xxNdqstHZ9wBFKYO9w8UQjtfzckkBNDLkaJ/WN0BoRubmUiV1+KwAyyBr6O\\nRnnZ9Tvy8VraSNSdJhX36ai36y18/sT6PWOp99zHYuDyz89KIz1la/fT9eSoR0Jy\\nYePmTEi+9pWhvtpAkqJkRxe5IDz71JVsQ07KoVfzaQKBgQDzKKUd/0ujhv/B9MQf\\nHcwSeWu/XnQ4hlcwz8dTWQjBV8gv9l4yBj9Pra62rg/tQ7b5XKMt6lv/tWs1IpdA\\neMsySY4972VPrmggKXgCnyKckDUYydNtHAIj9buo6AV8rONaneYnGv5wpSsf3q2c\\nOZrkamRgbBkI+B2mZ2obH1oVlQKBgQC9w9HkrDMvZ5L/ilZmpsvoHNFlQwmDgNlN\\n0ej5QGID5rljRM3CcLNHdyQiKqvLA9MCpPEXb2vVJPdmquD12A7a9s0OwxB/dtOD\\nykofcTY0ZHEM1HEyYJGmdK4FvZuNU4o2/D268dePjtj1Xw3c5fs0bcDiGQMtjWlz\\n5hjBzMsyHwKBgGjrIsPcwlBfEcAo0u7yNnnKNnmuUcuJ+9kt7j3Cbwqty80WKvK+\\ny1agBIECfhDMZQkXtbk8JFIjf4y/zi+db1/VaTDEORy2jmtCOWw4KgEQIDj/7OBp\\nc2r8vupUovl2x+rzsrkw5pTIT+FCffqoyHLCjWkle2/pTzHb8Waekoo5AoGAbELk\\nYy5uwTO45Hr60fOEzzZpq/iz28dNshz4agL2KD2gNGcTcEO1tCbfgXKQsfDLmG2b\\ncgBKJ77AOl1wnDEYQIme8TYOGnojL8Pfx9Jh10AaUvR8Y/49+hYFFhdXQCiR6M69\\nNQM2NJuNYWdKVGUMjJu0+AjHDFzp9YonQ6Ffp4cCgYEAmVALALCjU9GjJymgJ0lx\\nD9LccVHMwf9NmR/sMg0XNePRbCEcMDHKdtVJ1zPGS5txuxY3sRb/tDpv7TfuitrU\\nAw0/2ooMzunaoF/HXo+C/+t+pfuqPqLK4sCCyezUlMfCcaPdwXN2FmbgsaFHfe7I\\n7sGEnS/d8wEgydMiptJEf9s=\\n-----END PRIVATE KEY-----\\n", + "client_email": "firebase-adminsdk@project_id.iam.gserviceaccount.com", + "client_id": "client_id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk%40project_id.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} +""" + + class GcmTestCase(testutils.TestCase): maxDiff = None @@ -151,10 +174,37 @@ def config_setup(self, config: Dict[str, Any]) -> None: "api_key": "kii", "fcm_options": {"content_available": True, "mutable_content": True}, } - config["apps"]["com.example.gcm.apiv1"] = { + self.service_account_file = tempfile.NamedTemporaryFile() + self.service_account_file.write(FAKE_SERVICE_ACCOUNT_FILE) + self.service_account_file.flush() + config["apps"]["com.example.gcm.apiv1.load_service_account"] = { "type": "tests.test_gcm.TestGcmPushkin", "api_version": "v1", "project_id": "example_project", + "service_account_file": self.service_account_file.name, + "fcm_options": { + "android": { + "notification": { + "body": { + "test body", + }, + }, + }, + "apns": { + "payload": { + "aps": { + "content-available": 1, + "mutable-content": 1, + "alert": "", + }, + }, + }, + }, + } + config["apps"]["com.example.gcm.apiv1"] = { + "type": "tests.test_gcm.TestGcmPushkinCredentialBypass", + "api_version": "v1", + "project_id": "example_project", "fcm_options": { "android": { "notification": { @@ -175,6 +225,9 @@ def config_setup(self, config: Dict[str, Any]) -> None: }, } + def tearDown(self) -> None: + self.service_account_file.close() + def get_test_pushkin(self, name: str) -> TestGcmPushkin: pushkin = self.sygnal.pushkins[name] assert isinstance(pushkin, TestGcmPushkin) @@ -459,3 +512,12 @@ def test_fcm_options(self) -> None: assert gcm.last_request_body is not None self.assertEqual(gcm.last_request_body["mutable_content"], True) self.assertEqual(gcm.last_request_body["content_available"], True) + + def test_load_api_v1_service_account(self) -> None: + """ + Tests that the configured service_account_file can be loaded successfully. + """ + self.apns_pushkin_snotif = MagicMock() + gcm = self.get_test_pushkin("com.example.gcm.apiv1.load_service_account") + assert gcm.credentials is not None + assert gcm.credentials.project_id == "test_project_id" # type: ignore[attr-defined]