Repository: ethyca/fidesops (language: python)

Issue:
Firebase Authentication



Discussion / hints:
We have a pro adaptor for this https://gitlab.com/ethyca/adaptors/-/blob/master/src/main/resources/integrations/generic/firebase.yaml
The pro adaptor only retrieves Firebase user information, but Firebase is a database itself. We should follow up on this request and verify if all we need is to retrieve the user information like we did before or approach this in a more generic way and treat this like one of our other Fidesops database connectors.
This is actually a bit confusing, but **Firebase** itself is an "app development platform", and within that platform are a couple of products like:
* Cloud Firestore (fully cloud database: https://firebase.google.com/products/firestore)
* Realtime Database (previous version of their database: https://firebase.google.com/products/realtime-database)
* Authentication (SaaS API for handling authn flows: https://firebase.google.com/products/auth)
* etc.

In this case, we're focused on the "Firebase Authentication" product, which is focused exclusively on just the user authentication APIs. You can see more here: https://firebase.google.com/docs/reference/rest/auth
noting here that Firebase Auth offers an [Admin SDK](https://firebase.google.com/docs/auth/admin) (with python support) that _seems_ like the recommended way of [managing users](https://firebase.google.com/docs/auth/admin/manage-users), which i believe is what we are looking to do here (the first use case they describe is ["retrieve user data"](https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data)). i want to bring up the _possibility_ of using a SaaS request override extension here to bypass the normal HTTP SaaS connector framework. adding a dependency on that SDK would be something we need to think hard about, but one potential mitigating course would be to implement the extension/override in the `-plus` product.

the reason i'm bringing up this potential path is that the [REST APIs for user management](https://firebase.google.com/docs/reference/rest/auth#section-get-account-info) seem to rely on having a user's `idToken`, which is an ephemeral token generated by [signing in with the user's `customToken`](https://firebase.google.com/docs/reference/rest/auth#section-verify-custom-token), and i haven't been able to totally pin down how _that's_ generated - although it too can seemingly be [done by the SDK](https://firebase.google.com/docs/auth/admin/create-custom-tokens). generating the `customToken` and using that to retrieve an ephemeral `idToken` does seem to be exactly the course that our legacy adaptor took, although it relies on a `FirebaseCustomTokenGenerateFunction` to generate that customToken, and i haven't been able to find that function's implementation with my limited knowledge of the pro/adaptors codebase. generally, although this path seems like it _may_ be feasible in fidesops, it's looking to me like it will also require some extension/overrides of the generic framework and potentially even a dependency on the SDK -- but i could be off-base on that. if that is the case, though, perhaps we are better off just bypassing the HTTP API altogether and using a simple ```user = auth.get_user_by_email(email)``` from the admin SDK.

my understanding here - of the use case, the firebase landscape, and our legacy implementation - is still very shaky at best, so these are more just thoughts i wanted to jot down before we head into the weekend and look to tackle this next week.
here's an [internal "design doc"](https://ethyca.atlassian.net/wiki/spaces/EN/pages/2475786259/Fidesops+Firebase+Auth+Integration+Design) (rough term) i've published with initial findings/questions/assumptions. there is more detail in the doc, but the two fundamental decisions for implementation are:
- do we use the firebase admin SDK or do we use the REST API for integration?
- what approach do we take for service-account auth?

i'll look to draft up prototypes for both the admin SDK and the REST API approach - both should be feasible. i'll make some assumptions about the service-account auth for now, that should be relatively easy to swap in/out later.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c595609c..990dd4f7a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,7 @@ The types of changes are:
 * Add authenticated route to get consent preferences [#1402](https://github.com/ethyca/fidesops/pull/1402)
 * Access and erasure support for Braze [#1248](https://github.com/ethyca/fidesops/pull/1248)
 * Admin UI: Persist Redux store to localStorage [#1401](https://github.com/ethyca/fidesops/pull/1409)
+* Access and update support for Firebase Auth [#1339](https://github.com/ethyca/fidesops/pull/1399)
 
 ### Removed
 
diff --git a/data/saas/config/firebase_auth_config.yml b/data/saas/config/firebase_auth_config.yml
new file mode 100644
index 000000000..89086bb6a
--- /dev/null
+++ b/data/saas/config/firebase_auth_config.yml
@@ -0,0 +1,48 @@
+saas_config:
+  fides_key: <instance_fides_key>
+  name: Firebase Auth SaaS Config
+  type: firebase_auth
+  description: A sample schema representing the Firebase Auth connector for Fidesops
+  version: 0.0.1
+
+  connector_params:
+    - name: domain
+      default_value: https://identitytoolkit.googleapis.com
+    - name: type
+      default_value: service_account
+    - name: project_id
+    - name: private_key_id
+    - name: private_key
+    - name: client_email
+    - name: client_id
+    - name: auth_uri
+      default_value: https://accounts.google.com/o/oauth2/auth
+    - name: token_uri
+      default_value: https://oauth2.googleapis.com/token
+    - name: auth_provider_x509_cert_url
+      default_value: https://www.googleapis.com/oauth2/v1/certs
+    - name: client_x509_cert_url
+
+
+  client_config:
+    protocol: NOT_USED
+    host: NOT_USED
+
+  test_request:
+    method: POST
+    path: /v1/accounts:createAuthUri
+
+
+  endpoints:
+    - name: user
+      requests:
+        read:
+          request_override: firebase_auth_user_access
+          param_values:
+            - name: email
+              identity: email
+        update:
+          request_override: firebase_auth_user_update
+          param_values:
+            - name: email
+              identity: email
diff --git a/data/saas/dataset/firebase_auth_dataset.yml b/data/saas/dataset/firebase_auth_dataset.yml
new file mode 100644
index 000000000..578483fb2
--- /dev/null
+++ b/data/saas/dataset/firebase_auth_dataset.yml
@@ -0,0 +1,57 @@
+dataset:
+  - fides_key: <instance_fides_key>
+    name: Firebase Auth Dataset
+    description: A sample dataset representing the Firebase Auth connector for Fidesops
+    collections:
+      - name: user
+        fields:
+          - name: uid
+            data_categories: [system.operations]
+            fidesops_meta:
+              data_type: string
+              primary_key: True
+          - name: email
+            data_categories: [user.contact.email]
+            fidesops_meta:
+              data_type: string
+          - name: email_verified
+            data_categories: [system.operations]
+            fidesops_meta:
+              data_type: boolean
+          - name: display_name
+            data_categories: [user.name]
+            fidesops_meta:
+              data_type: string
+          - name: phone_number
+            data_categories: [user.contact.phone_number]
+            fidesops_meta:
+              data_type: string
+          - name: photo_url
+            data_categories: [user]
+            fidesops_meta:
+              data_type: string
+          - name: disabled
+            data_categories: [system.operations]
+            fidesops_meta:
+              data_type: boolean
+          - name: provider_data
+            fidesops_meta:
+              data_type: object[]
+            fields:
+              - name: provider_id
+                data_categories: [system.operations]
+                fidesops_meta:
+                  data_type: string
+              - name: display_name
+                data_categories: [user.name]
+                fidesops_meta:
+                  data_type: string
+              - name: email
+                data_categories: [user.contact.email]
+                fidesops_meta:
+                  data_type: string
+              - name: photo_url
+                data_categories: [user]
+                fidesops_meta:
+                  data_type: string
+
diff --git a/data/saas/saas_connector_registry.toml b/data/saas/saas_connector_registry.toml
index 24d8dc6e5..59d6ad91b 100644
--- a/data/saas/saas_connector_registry.toml
+++ b/data/saas/saas_connector_registry.toml
@@ -16,6 +16,12 @@ dataset = "data/saas/dataset/datadog_dataset.yml"
 icon = "data/saas/icon/default.svg"
 human_readable = "Datadog"
 
+[firebase_auth]
+config = "data/saas/config/firebase_auth_config.yml"
+dataset = "data/saas/dataset/firebase_auth_dataset.yml"
+icon = "data/saas/icon/default.svg"
+human_readable = "Firebase Auth"
+
 [hubspot]
 config = "data/saas/config/hubspot_config.yml"
 dataset = "data/saas/dataset/hubspot_dataset.yml"
diff --git a/pyproject.toml b/pyproject.toml
index 028d4a88b..042c4b048 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,6 +39,7 @@ module = [
   "fideslang.*",
   "fideslib.*",
   "fideslog.*",
+  "firebase_admin.*",
   "jose.*",
   "jwt.*",
   "multidimensional_urlencode.*",
diff --git a/requirements.txt b/requirements.txt
index c0f68568a..0625647a9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,6 +11,7 @@ fastapi[all]==0.82.0
 fideslang==1.2.0
 fideslib==3.1.4
 fideslog==1.2.3
+firebase-admin==5.3.0
 hvac==0.11.2
 Jinja2==3.1.2
 multidimensional_urlencode==0.0.4
diff --git a/src/fidesops/ops/schemas/saas/saas_config.py b/src/fidesops/ops/schemas/saas/saas_config.py
index f788d35be..8c5679fad 100644
--- a/src/fidesops/ops/schemas/saas/saas_config.py
+++ b/src/fidesops/ops/schemas/saas/saas_config.py
@@ -264,6 +264,7 @@ class SaaSType(Enum):
     datadog = "datadog"
     rollbar = "rollbar"
     braze = "braze"
+    firebase_auth = "firebase_auth"
 
 
 class SaaSConfigBase(BaseModel):
diff --git a/src/fidesops/ops/service/saas_request/__init__.py b/src/fidesops/ops/service/saas_request/__init__.py
index fad5bf1f2..ead6411e5 100644
--- a/src/fidesops/ops/service/saas_request/__init__.py
+++ b/src/fidesops/ops/service/saas_request/__init__.py
@@ -1,3 +1,4 @@
 from fidesops.ops.service.saas_request.override_implementations import (
+    firebase_auth_request_overrides,
     mailchimp_request_overrides,
 )
diff --git a/src/fidesops/ops/service/saas_request/override_implementations/firebase_auth_request_overrides.py b/src/fidesops/ops/service/saas_request/override_implementations/firebase_auth_request_overrides.py
new file mode 100644
index 000000000..f1a7bdd04
--- /dev/null
+++ b/src/fidesops/ops/service/saas_request/override_implementations/firebase_auth_request_overrides.py
@@ -0,0 +1,166 @@
+import logging
+from typing import Any, Dict, List
+
+import firebase_admin
+from firebase_admin import App, auth, credentials
+from firebase_admin.auth import UserRecord
+
+from fidesops.ops.graph.traversal import TraversalNode
+from fidesops.ops.models.policy import Policy
+from fidesops.ops.models.privacy_request import PrivacyRequest
+from fidesops.ops.service.saas_request.saas_request_override_factory import (
+    SaaSRequestType,
+    register,
+)
+from fidesops.ops.util.collection_util import Row
+
+logger = logging.getLogger(__name__)
+
+
+@register("firebase_auth_user_access", [SaaSRequestType.READ])
+def firebase_auth_user_access(  # pylint: disable=R0914
+    node: TraversalNode,
+    policy: Policy,
+    privacy_request: PrivacyRequest,
+    input_data: Dict[str, List[Any]],
+    secrets: Dict[str, Any],
+) -> List[Row]:
+    """
+    SaaS Request Override function used to integrate access requests with
+    Firebase Auth service. Specifically, it reads `User` records.
+    """
+    app = initialize_firebase(secrets)
+
+    processed_data = []
+
+    emails = input_data.get("email", [])
+    for email in emails:
+        user: UserRecord = auth.get_user_by_email(email, app=app)
+        row = {"email": user.email, "uid": user.uid}
+        keys = [
+            "email",
+            "uid",
+            "display_name",
+            "phone_number",
+            "photo_url",
+            "disabled",
+            "email_verified",
+        ]
+        for key in keys:
+            value = getattr(user, key)
+            if value is not None:
+                row[key] = value
+
+        if user.provider_data:
+            pds = []
+            for pd in user.provider_data:
+                pd_keys = ["display_name", "provider_id", "email", "photo_url"]
+                pd_to_add = {}
+                for key in pd_keys:
+                    value = getattr(pd, key)
+                    if value is not None:
+                        pd_to_add[key] = value
+                pds.append(pd_to_add)
+            row["provider_data"] = pds
+        processed_data.append(row)
+    return processed_data
+
+
+@register("firebase_auth_user_update", [SaaSRequestType.UPDATE])
+def firebase_auth_user_update(
+    param_values_per_row: List[Dict[str, Any]],
+    policy: Policy,
+    privacy_request: PrivacyRequest,
+    secrets: Dict[str, Any],
+) -> int:
+    """
+    SaaS Request Override function used to integrate update requests with
+    Firebase Auth service. Specifically, it updates `User` records.
+    """
+    app = initialize_firebase(secrets)
+    rows_updated = 0
+    # each update_params dict correspond to a record that needs to be updated
+    for row_param_values in param_values_per_row:
+        email = row_param_values.get("email")
+        user: UserRecord = auth.get_user_by_email(email, app=app)
+        masked_fields = row_param_values["masked_object_fields"]
+        display_name = masked_fields.get("display_name", user.display_name)
+        phone_number = masked_fields.get("phone_number", user.phone_number)
+        photo_url = masked_fields.get("photo_url", user.photo_url)
+        disabled = masked_fields.get("disabled", user.disabled)
+        email_verified = masked_fields.get("email_verified", user.email_verified)
+        auth.update_user(
+            user.uid,
+            email=email,
+            display_name=display_name,
+            phone_number=phone_number,
+            photo_url=photo_url,
+            disabled=disabled,
+            email_verified=email_verified,
+            app=app,
+        )
+        rows_updated += 1
+    return rows_updated
+
+
+@register("firebase_auth_user_delete", [SaaSRequestType.DELETE])
+def firebase_auth_user_delete(
+    param_values_per_row: List[Dict[str, Any]],
+    policy: Policy,
+    privacy_request: PrivacyRequest,
+    secrets: Dict[str, Any],
+) -> int:
+    """
+    SaaS Request Override function used to integrate delete requests with
+    Firebase Auth service. Specifically, it deletes `User` records.
+
+    Currently, this function is not invoked by any default configuration -
+    instead, Firebase erasures are done via updates, and the update function
+    defined above. We've kept this function implementation here so that
+    it can be easily leveraged without code changes and by a simple config update.
+    """
+    app = initialize_firebase(secrets)
+    rows_updated = 0
+    # each update_params dict correspond to a record that needs to be updated
+    for row_param_values in param_values_per_row:
+        email = row_param_values.get("email")
+        user: UserRecord = auth.get_user_by_email(email, app=app)
+        auth.delete_user(user.uid, app=app)
+        rows_updated += 1
+    return rows_updated
+
+
+def initialize_firebase(secrets: Dict[str, Any]) -> App:
+    """
+    Initializes a Firebase "app" instance based on the given
+    secrets `dict`. The value of the `project_id` key of the
+    secrets dict is used for the `name` of the Firebase app.
+    """
+    creds = get_firebase_credentials(secrets)
+    project_id = creds.project_id
+    try:
+        app = firebase_admin.get_app(project_id)
+    except ValueError:
+        app = firebase_admin.initialize_app(credential=creds, name=project_id)
+    return app
+
+
+def get_firebase_credentials(secrets: Dict[str, Any]) -> credentials.Certificate:
+    """
+    Utility function to create a Firebase `credentials.Certificate` instance
+    based on the provided secrets `dict`.
+    """
+    credential_keys = [
+        "type",
+        "project_id",
+        "private_key_id",
+        "private_key",
+        "client_email",
+        "client_id",
+        "auth_uri",
+        "token_uri",
+        "auth_provider_x509_cert_url",
+        "client_x509_cert_url",
+    ]
+    creds = {key: secrets[key] for key in credential_keys}
+    return credentials.Certificate(cert=creds)


Test patch:
diff --git a/tests/ops/api/v1/endpoints/test_connection_template_endpoints.py b/tests/ops/api/v1/endpoints/test_connection_template_endpoints.py
index f05080532..671d59257 100644
--- a/tests/ops/api/v1/endpoints/test_connection_template_endpoints.py
+++ b/tests/ops/api/v1/endpoints/test_connection_template_endpoints.py
@@ -48,7 +48,7 @@ def test_get_connection_types(
         resp = api_client.get(url, headers=auth_header)
         data = resp.json()["items"]
         assert resp.status_code == 200
-        assert len(data) == 25
+        assert len(data) == 26
 
         assert {
             "identifier": ConnectionType.postgres.value,
@@ -82,7 +82,7 @@ def test_search_connection_types(self, api_client, generate_auth_header, url):
         resp = api_client.get(url + "?search=re", headers=auth_header)
         assert resp.status_code == 200
         data = resp.json()["items"]
-        assert len(data) == 3
+        assert len(data) == 4
         assert data == [
             {
                 "identifier": ConnectionType.postgres.value,
@@ -94,6 +94,11 @@ def test_search_connection_types(self, api_client, generate_auth_header, url):
                 "type": SystemType.database.value,
                 "human_readable": "Amazon Redshift",
             },
+            {
+                "identifier": SaaSType.firebase_auth.value,
+                "type": SystemType.saas.value,
+                "human_readable": "Firebase Auth",
+            },
             {
                 "identifier": SaaSType.outreach.value,
                 "type": SystemType.saas.value,
@@ -124,7 +129,7 @@ def test_search_connection_types_case_insensitive(
         resp = api_client.get(url + "?search=Re", headers=auth_header)
         assert resp.status_code == 200
         data = resp.json()["items"]
-        assert len(data) == 3
+        assert len(data) == 4
         assert data == [
             {
                 "identifier": ConnectionType.postgres.value,
@@ -136,6 +141,11 @@ def test_search_connection_types_case_insensitive(
                 "type": SystemType.database.value,
                 "human_readable": "Amazon Redshift",
             },
+            {
+                "identifier": SaaSType.firebase_auth.value,
+                "type": SystemType.saas.value,
+                "human_readable": "Firebase Auth",
+            },
             {
                 "identifier": SaaSType.outreach.value,
                 "type": SystemType.saas.value,
@@ -152,7 +162,7 @@ def test_search_system_type(self, api_client, generate_auth_header, url):
         resp = api_client.get(url + "?system_type=saas", headers=auth_header)
         assert resp.status_code == 200
         data = resp.json()["items"]
-        assert len(data) == 15
+        assert len(data) == 16
 
         resp = api_client.get(url + "?system_type=database", headers=auth_header)
         assert resp.status_code == 200
diff --git a/tests/ops/conftest.py b/tests/ops/conftest.py
index 8c25d8dac..e7e325bad 100644
--- a/tests/ops/conftest.py
+++ b/tests/ops/conftest.py
@@ -48,6 +48,7 @@
 from .fixtures.saas.hubspot_fixtures import *
 from .fixtures.saas.mailchimp_fixtures import *
 from .fixtures.saas.outreach_fixtures import *
+from .fixtures.saas.request_override.firebase_auth_fixtures import *
 from .fixtures.saas.request_override.mailchimp_override_fixtures import *
 from .fixtures.saas.rollbar_fixtures import *
 from .fixtures.saas.salesforce_fixtures import *
diff --git a/tests/ops/fixtures/saas/request_override/firebase_auth_fixtures.py b/tests/ops/fixtures/saas/request_override/firebase_auth_fixtures.py
new file mode 100644
index 000000000..4ae6aee49
--- /dev/null
+++ b/tests/ops/fixtures/saas/request_override/firebase_auth_fixtures.py
@@ -0,0 +1,171 @@
+from typing import Any, Dict, Generator
+
+import pydash
+import pytest
+from fideslib.cryptography import cryptographic_util
+from fideslib.db import session
+from firebase_admin import auth
+from firebase_admin.auth import UserNotFoundError
+from firebase_admin.exceptions import FirebaseError
+from sqlalchemy.orm import Session
+
+from fidesops.ops.models.connectionconfig import (
+    AccessLevel,
+    ConnectionConfig,
+    ConnectionType,
+)
+from fidesops.ops.models.datasetconfig import DatasetConfig
+from fidesops.ops.service.saas_request.override_implementations.firebase_auth_request_overrides import (
+    initialize_firebase,
+)
+from fidesops.ops.util.saas_util import (
+    load_config_with_replacement,
+    load_dataset_with_replacement,
+)
+from tests.ops.test_helpers.vault_client import get_secrets
+
+secrets = get_secrets("firebase_auth")
+
+
+@pytest.fixture
+def firebase_auth_config() -> Dict[str, Any]:
+    return load_config_with_replacement(
+        "data/saas/config/firebase_auth_config.yml",
+        "<instance_fides_key>",
+        "firebase_auth_instance",
+    )
+
+
+@pytest.fixture
+def firebase_auth_dataset() -> Dict[str, Any]:
+    return load_dataset_with_replacement(
+        "data/saas/dataset/firebase_auth_dataset.yml",
+        "<instance_fides_key>",
+        "firebase_auth_instance",
+    )[0]
+
+
+@pytest.fixture(scope="session")
+def firebase_auth_secrets(saas_config):
+
+    return {
+        "domain": pydash.get(saas_config, "firebase_auth.domain") or secrets["domain"],
+        "type": pydash.get(saas_config, "firebase_auth.type") or secrets["type"],
+        "project_id": pydash.get(saas_config, "firebase_auth.project_id")
+        or secrets["project_id"],
+        "private_key_id": pydash.get(saas_config, "firebase_auth.private_key_id")
+        or secrets["private_key_id"],
+        "private_key": pydash.get(saas_config, "firebase_auth.private_key")
+        or secrets["private_key"],
+        "client_email": pydash.get(saas_config, "firebase_auth.client_email")
+        or secrets["client_email"],
+        "client_id": pydash.get(saas_config, "firebase_auth.client_id")
+        or secrets["client_id"],
+        "auth_uri": pydash.get(saas_config, "firebase_auth.auth_uri")
+        or secrets["auth_uri"],
+        "token_uri": pydash.get(saas_config, "firebase_auth.token_uri")
+        or secrets["token_uri"],
+        "auth_provider_x509_cert_url": pydash.get(
+            saas_config, "firebase_auth.auth_provider_x509_cert_url"
+        )
+        or secrets["auth_provider_x509_cert_url"],
+        "client_x509_cert_url": pydash.get(
+            saas_config, "firebase_auth.client_x509_cert_url"
+        )
+        or secrets["client_x509_cert_url"],
+    }
+
+
+@pytest.fixture(scope="session")
+def firebase_auth_user(firebase_auth_secrets) -> Generator:
+    app = initialize_firebase(firebase_auth_secrets)
+
+    # create a user provider
+    uid = cryptographic_util.generate_secure_random_string(28)
+    email = f"{cryptographic_util.generate_secure_random_string(13)}@email.com"
+    provider_id = "facebook.com"
+    display_name = "John Doe #1"
+    photo_url = "http://www.facebook.com/12345678/photo.png"
+    up1 = auth.UserProvider(
+        uid,
+        email=email,
+        provider_id=provider_id,
+        display_name=display_name,
+        photo_url=photo_url,
+    )
+
+    # create another user provider
+    uid = cryptographic_util.generate_secure_random_string(28)
+    email = f"{cryptographic_util.generate_secure_random_string(13)}@email.com"
+    provider_id = "google.com"
+    display_name = "John Doe #2"
+    up2 = auth.UserProvider(
+        uid,
+        email=email,
+        provider_id=provider_id,
+        display_name=display_name,
+    )
+    # create the user
+    email = f"{cryptographic_util.generate_secure_random_string(13)}@email.com"
+    uid = cryptographic_util.generate_secure_random_string(28)
+    user = auth.ImportUserRecord(
+        uid=uid,
+        email=email,
+        email_verified=False,
+        display_name="John Doe",
+        photo_url="http://www.example.com/12345678/photo.png",
+        disabled=False,
+        provider_data=[up1, up2],
+    )
+    auth.import_users([user], app=app)
+
+    yield user
+
+    try:
+        auth.delete_user(user.uid, app=app)
+    except FirebaseError as e:
+        # user may have already been deleted, so catch the possible exception
+        if not isinstance(e, UserNotFoundError):
+            raise e
+
+
+@pytest.fixture(scope="function")
+def firebase_auth_connection_config(
+    db: session, firebase_auth_config, firebase_auth_secrets
+) -> Generator:
+    fides_key = firebase_auth_config["fides_key"]
+    connection_config = ConnectionConfig.create(
+        db=db,
+        data={
+            "key": fides_key,
+            "name": fides_key,
+            "connection_type": ConnectionType.saas,
+            "access": AccessLevel.write,
+            "secrets": firebase_auth_secrets,
+            "saas_config": firebase_auth_config,
+        },
+    )
+    yield connection_config
+    connection_config.delete(db)
+
+
+@pytest.fixture
+def firebase_auth_dataset_config(
+    db: Session,
+    firebase_auth_connection_config: ConnectionConfig,
+    firebase_auth_dataset: Dict[str, Any],
+) -> Generator:
+    fides_key = firebase_auth_dataset["fides_key"]
+    firebase_auth_connection_config.name = fides_key
+    firebase_auth_connection_config.key = fides_key
+    firebase_auth_connection_config.save(db=db)
+    dataset = DatasetConfig.create(
+        db=db,
+        data={
+            "connection_config_id": firebase_auth_connection_config.id,
+            "fides_key": fides_key,
+            "dataset": firebase_auth_dataset,
+        },
+    )
+    yield dataset
+    dataset.delete(db=db)
diff --git a/tests/ops/integration_tests/saas/request_override/test_firebase_auth_task.py b/tests/ops/integration_tests/saas/request_override/test_firebase_auth_task.py
new file mode 100644
index 000000000..9fe75cce5
--- /dev/null
+++ b/tests/ops/integration_tests/saas/request_override/test_firebase_auth_task.py
@@ -0,0 +1,220 @@
+import random
+
+import pytest
+from firebase_admin import auth
+from firebase_admin.auth import UserNotFoundError, UserRecord
+
+from fidesops.ops.graph.graph import DatasetGraph
+from fidesops.ops.models.privacy_request import PrivacyRequest
+from fidesops.ops.schemas.redis_cache import Identity
+from fidesops.ops.service.saas_request.override_implementations.firebase_auth_request_overrides import (
+    firebase_auth_user_delete,
+    initialize_firebase,
+)
+from fidesops.ops.task import graph_task
+from fidesops.ops.task.graph_task import get_cached_data_for_erasures
+from tests.ops.graph.graph_test_util import assert_rows_match
+
+
+@pytest.mark.integration_saas
+@pytest.mark.integration_saas_override
+@pytest.mark.asyncio
+async def test_firebase_auth_access_request(
+    db,
+    policy,
+    firebase_auth_connection_config,
+    firebase_auth_dataset_config,
+    firebase_auth_user: auth.ImportUserRecord,
+) -> None:
+    """Full access request based on the Firebase Auth SaaS config"""
+
+    privacy_request = PrivacyRequest(
+        id=f"test_firebase_access_request_task_{random.randint(0, 1000)}"
+    )
+    identity = Identity(**{"email": firebase_auth_user.email})
+    privacy_request.cache_identity(identity)
+
+    dataset_name = firebase_auth_connection_config.get_saas_config().fides_key
+    merged_graph = firebase_auth_dataset_config.get_graph()
+    graph = DatasetGraph(merged_graph)
+
+    v = await graph_task.run_access_request(
+        privacy_request,
+        policy,
+        graph,
+        [firebase_auth_connection_config],
+        {"email": firebase_auth_user.email},
+        db,
+    )
+
+    assert_rows_match(
+        v[f"{dataset_name}:user"],
+        min_size=1,
+        keys=[
+            "uid",
+            "email",
+            "display_name",
+            "photo_url",
+            "disabled",
+            "email_verified",
+        ],
+    )
+    response_user = v[f"{dataset_name}:user"][0]
+    assert response_user["uid"] == firebase_auth_user.uid
+    assert response_user["email"] == firebase_auth_user.email
+    assert response_user["display_name"] == firebase_auth_user.display_name
+    assert response_user["photo_url"] == firebase_auth_user.photo_url
+    assert response_user["disabled"] == firebase_auth_user.disabled
+    assert response_user["email_verified"] == firebase_auth_user.email_verified
+    assert "phone_number" not in response_user.keys()
+
+    provider_data = response_user["provider_data"]
+    assert (
+        provider_data[0]["provider_id"]
+        == firebase_auth_user.provider_data[0].provider_id
+    )
+    assert (
+        provider_data[0]["display_name"]
+        == firebase_auth_user.provider_data[0].display_name
+    )
+    assert provider_data[0]["email"] == firebase_auth_user.provider_data[0].email
+    assert (
+        provider_data[0]["photo_url"] == firebase_auth_user.provider_data[0].photo_url
+    )
+    assert (
+        provider_data[1]["provider_id"]
+        == firebase_auth_user.provider_data[1].provider_id
+    )
+    assert (
+        provider_data[1]["display_name"]
+        == firebase_auth_user.provider_data[1].display_name
+    )
+    assert provider_data[1]["email"] == firebase_auth_user.provider_data[1].email
+    assert "photo_url" not in provider_data[1].keys()
+
+
+@pytest.mark.integration_saas
+@pytest.mark.integration_saas_override
+@pytest.mark.asyncio
+async def test_firebase_auth_update_request(
+    db,
+    policy,
+    firebase_auth_connection_config,
+    firebase_auth_dataset_config,
+    firebase_auth_user: auth.ImportUserRecord,
+    erasure_policy_string_rewrite,
+    firebase_auth_secrets,
+) -> None:
+    """Update request based on the Firebase Auth SaaS config"""
+
+    privacy_request = PrivacyRequest(
+        id=f"test_firebase_update_request_task_{random.randint(0, 1000)}"
+    )
+    identity = Identity(**{"email": firebase_auth_user.email})
+    privacy_request.cache_identity(identity)
+
+    dataset_name = firebase_auth_connection_config.get_saas_config().fides_key
+    merged_graph = firebase_auth_dataset_config.get_graph()
+    graph = DatasetGraph(merged_graph)
+
+    v = await graph_task.run_access_request(
+        privacy_request,
+        policy,
+        graph,
+        [firebase_auth_connection_config],
+        {"email": firebase_auth_user.email},
+        db,
+    )
+
+    assert_rows_match(
+        v[f"{dataset_name}:user"],
+        min_size=1,
+        keys=[
+            "uid",
+            "email",
+            "display_name",
+            "disabled",
+            "email_verified",
+        ],
+    )
+
+    v = await graph_task.run_erasure(
+        privacy_request,
+        erasure_policy_string_rewrite,
+        graph,
+        [firebase_auth_connection_config],
+        {"email": firebase_auth_user.email},
+        get_cached_data_for_erasures(privacy_request.id),
+        db,
+    )
+
+    app = initialize_firebase(firebase_auth_secrets)
+    user: UserRecord = auth.get_user(firebase_auth_user.uid, app=app)
+    assert user.display_name == "MASKED"
+    assert user.email == firebase_auth_user.email
+    assert user.photo_url == firebase_auth_user.photo_url
+    assert user.disabled == firebase_auth_user.disabled
+    assert user.email_verified == firebase_auth_user.email_verified
+
+    assert (
+        user.provider_data[0].provider_id
+        == firebase_auth_user.provider_data[0].provider_id
+    )
+    assert (
+        user.provider_data[0].display_name
+        == firebase_auth_user.provider_data[0].display_name
+    )
+    assert user.provider_data[0].email == firebase_auth_user.provider_data[0].email
+    assert (
+        user.provider_data[0].photo_url == firebase_auth_user.provider_data[0].photo_url
+    )
+    assert (
+        user.provider_data[1].provider_id
+        == firebase_auth_user.provider_data[1].provider_id
+    )
+    assert (
+        user.provider_data[1].display_name
+        == firebase_auth_user.provider_data[1].display_name
+    )
+    assert user.provider_data[1].email == firebase_auth_user.provider_data[1].email
+    assert (
+        user.provider_data[1].photo_url == firebase_auth_user.provider_data[1].photo_url
+    )
+
+
+@pytest.mark.integration_saas
+@pytest.mark.integration_saas_override
+@pytest.mark.asyncio
+async def test_firebase_auth_delete_request(
+    db,
+    policy,
+    firebase_auth_connection_config,
+    firebase_auth_dataset_config,
+    firebase_auth_user: auth.UserRecord,
+    erasure_policy_string_rewrite,
+    firebase_auth_secrets,
+) -> None:
+    """
+    Tests delete functionality by explicitly invoking the delete override function
+
+    We can't have an'end-to-end' privacy request test, as preferred, because
+    our delete function is not configured by default (the udpate function is).
+    But this at least gives us some test coverage of the delete function directly.
+    """
+    param_values_per_row = [{"email": firebase_auth_user.email}]
+    app = initialize_firebase(firebase_auth_secrets)
+
+    # assert user exists
+    user_record: UserRecord = auth.get_user_by_email(firebase_auth_user.email, app=app)
+    assert user_record.email == firebase_auth_user.email
+    assert user_record.uid == firebase_auth_user.uid
+
+    # run the delete function
+    firebase_auth_user_delete(param_values_per_row, None, None, firebase_auth_secrets)
+
+    # confirm the user no longer exists
+    with pytest.raises(UserNotFoundError):
+        auth.get_user_by_email(firebase_auth_user.email, app=app)
+
+    with pytest.raises(UserNotFoundError):
+        auth.get_user(firebase_auth_user.uid, app=app)