Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
cd09324
Adding base executionlog saas version
Vagoasdf Mar 12, 2026
85ab8eb
adding execution log saas version model
Vagoasdf Mar 12, 2026
396c00a
adding saas_version on the logs
Vagoasdf Mar 12, 2026
a39d035
adding test for the execution log
Vagoasdf Mar 12, 2026
db4557d
Adding Test for the graph task
Vagoasdf Mar 12, 2026
b5d0ea7
adding logging on the graph task
Vagoasdf Mar 12, 2026
9899f9d
adding saas_version to the audit query
Vagoasdf Mar 12, 2026
7baae34
Fixing Revision name and settings
Vagoasdf Mar 13, 2026
1053fb4
Caching Saas version
Vagoasdf Mar 13, 2026
4600544
Updating tests
Vagoasdf Mar 13, 2026
98a5629
Event logs UI adding version
Vagoasdf Mar 16, 2026
a9ca916
Updating dabase dataset.
Vagoasdf Mar 16, 2026
be47458
using terniaries and avoiding long expressions
Vagoasdf Mar 16, 2026
f536355
Removing unnecesary policy
Vagoasdf Mar 16, 2026
e5a4515
Adding version in its own column
Vagoasdf Mar 16, 2026
caee311
Adding changelog
Vagoasdf Mar 16, 2026
519350f
Merge branch 'main' into ENG-567_record-integration-version-on-exec-logs
Vagoasdf Mar 16, 2026
47253a2
Updating revision
Vagoasdf Mar 16, 2026
db3babc
running ruff
Vagoasdf Mar 16, 2026
39ecea6
Adding saas_versions to endpoint tests
Vagoasdf Mar 17, 2026
5ae13c1
Merge branch 'main' into ENG-567_record-integration-version-on-exec-logs
Vagoasdf Mar 17, 2026
ca3149d
Renaming migration
Vagoasdf Mar 17, 2026
efe6d6b
Updating down revision
Vagoasdf Mar 17, 2026
d564b9b
Fixing up greptile comments on tests
Vagoasdf Mar 17, 2026
836d76a
Renaming migration
Vagoasdf Mar 17, 2026
74522d5
Addressing code review feedback
Vagoasdf Mar 18, 2026
f51ff8b
Base saas_config_version model
Vagoasdf Mar 13, 2026
13ff63f
Adding migration for the config version tracker
Vagoasdf Mar 16, 2026
a978bb5
Base saas config versionn model
Vagoasdf Mar 16, 2026
0b72eb0
setting up out of the box sas config version on seed
Vagoasdf Mar 16, 2026
bc1e376
Updating saas template
Vagoasdf Mar 16, 2026
7df7977
creating new endpoints for saas_config_versions
Vagoasdf Mar 16, 2026
37ea6f6
Adding model and schemas for connection_config_saas_history
Vagoasdf Mar 17, 2026
57543df
Adding saas_version_history endpoints
Vagoasdf Mar 17, 2026
0d2296b
Addressing code review feedback on PR 2
Vagoasdf Mar 18, 2026
0855e30
Merge branch 'main' into ENG-567_pr2-saas-config-version
Vagoasdf Mar 24, 2026
7cf7d3f
Merge branch 'main' into ENG-567_pr2-saas-config-version
Vagoasdf Mar 24, 2026
c5b4165
Fixing up base migration frm PR split
Vagoasdf Mar 25, 2026
de4b4f2
Running static checks
Vagoasdf Mar 25, 2026
a289f97
Merge branch 'main' into ENG-567_pr2-saas-config-version
Vagoasdf Mar 25, 2026
28082df
Merge branch 'main' into ENG-567_pr2-saas-config-version
Vagoasdf Mar 25, 2026
6fe472f
Fix CI: ruff imports, mypy return type, db annotations, changelog
Vagoasdf Mar 25, 2026
5b870f7
Merge branch 'main' into ENG-567_pr2-saas-config-version
Vagoasdf Mar 25, 2026
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
36 changes: 36 additions & 0 deletions .fides/db_dataset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,24 @@ dataset:
- name: enabled_actions
description: 'The privacy actions that are enabled for this connection'
data_categories: [system.operations]
- name: connection_config_saas_history
fields:
- name: id
data_categories: [system.operations]
- name: created_at
data_categories: [system.operations]
- name: updated_at
data_categories: [system.operations]
- name: connection_config_id
data_categories: [system.operations]
- name: connection_key
data_categories: [system.operations]
- name: version
data_categories: [system.operations]
- name: config
data_categories: [system.operations]
- name: dataset
data_categories: [system.operations]
- name: consent
description: 'A database table used to map consent preference to identities'
data_categories: []
Expand Down Expand Up @@ -3841,6 +3859,24 @@ dataset:
data_categories: [user.unique_id]
- name: is_default
data_categories: [system.operations]
- name: saas_config_version
fields:
- name: id
data_categories: [system.operations]
- name: created_at
data_categories: [system.operations]
- name: updated_at
data_categories: [system.operations]
- name: connector_type
data_categories: [system.operations]
- name: version
data_categories: [system.operations]
- name: config
data_categories: [system.operations]
- name: dataset
data_categories: [system.operations]
- name: is_custom
data_categories: [system.operations]
- name: saas_template_dataset
fields:
- name: id
Expand Down
4 changes: 4 additions & 0 deletions changelog/7688-saas-config-version-history.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type: Added
description: Added SaaS config version history tracking — stores connector template versions on seed and custom template upload, with API endpoints to list versions and retrieve config/dataset snapshots by version
pr: 7688
labels: ["db-migration"]
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Only populated for SaaS connectors (connection_type = 'saas'); null for all others.

Revision ID: a1ca9ddf3c3c
Revises: 94273d7e8319
Revises: ad1bb600715b
Create Date: 2026-03-11

"""
Expand All @@ -16,6 +16,7 @@
# revision identifiers, used by Alembic.
revision = "a1ca9ddf3c3c"
down_revision = "ad1bb600715b"

branch_labels = None
depends_on = None

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""add saas_config_version table

Stores a snapshot of each SaaS integration config and dataset per
(connector_type, version) pair. Rows are written on startup for bundled
OOB connectors, on custom template upload/update, and on direct SaaS config
PATCH. Rows are never deleted so that execution logs can always resolve the
config/dataset that was active when a DSR ran.

Revision ID: c3e5f7a9b1d2
Revises: a1ca9ddf3c3c
Create Date: 2026-03-13

"""

import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "c3e5f7a9b1d2"
down_revision = "a1ca9ddf3c3c"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"saas_config_version",
sa.Column("id", sa.String(length=255), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True),
sa.Column("connector_type", sa.String(), nullable=False),
sa.Column("version", sa.String(), nullable=False),
sa.Column("config", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column("dataset", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column("is_custom", sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("connector_type", "version", "is_custom", name="uq_saas_config_version"),
)
op.create_index(
op.f("ix_saas_config_version_connector_type"),
"saas_config_version",
["connector_type"],
unique=False,
)
op.create_index(
op.f("ix_saas_config_version_id"),
"saas_config_version",
["id"],
unique=False,
)


def downgrade() -> None:
op.drop_index(op.f("ix_saas_config_version_id"), table_name="saas_config_version")
op.drop_index(op.f("ix_saas_config_version_connector_type"), table_name="saas_config_version")
op.drop_table("saas_config_version")
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""add connection_config_saas_history table

Stores a per-connection snapshot of a SaaS config (and associated datasets)
each time ConnectionConfig.update_saas_config() is called. Unlike the
template-level saas_config_version table, this table is append-only and
scoped to an individual connection instance, so divergent configs are
preserved correctly.

Revision ID: d4e6f8a0b2c3
Revises: c3e5f7a9b1d2
Create Date: 2026-03-17

"""

import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "d4e6f8a0b2c3"
down_revision = "c3e5f7a9b1d2"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"connection_config_saas_history",
sa.Column("id", sa.String(length=255), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column("connection_config_id", sa.String(length=255), nullable=True),
sa.Column("connection_key", sa.String(), nullable=False),
sa.Column("version", sa.String(), nullable=False),
sa.Column(
"config", postgresql.JSONB(astext_type=sa.Text()), nullable=False
),
sa.Column(
"dataset", postgresql.JSONB(astext_type=sa.Text()), nullable=True
),
sa.ForeignKeyConstraint(
["connection_config_id"],
["connectionconfig.id"],
ondelete="SET NULL",
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_connection_config_saas_history_id"),
"connection_config_saas_history",
["id"],
unique=False,
)
op.create_index(
op.f("ix_connection_config_saas_history_connection_config_id"),
"connection_config_saas_history",
["connection_config_id"],
unique=False,
)


def downgrade() -> None:
op.drop_index(
op.f("ix_connection_config_saas_history_connection_config_id"),
table_name="connection_config_saas_history",
)
op.drop_index(
op.f("ix_connection_config_saas_history_id"),
table_name="connection_config_saas_history",
)
op.drop_table("connection_config_saas_history")
2 changes: 2 additions & 0 deletions src/fides/api/db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from fides.api.models.chat_config import ChatConfig
from fides.api.models.client import ClientDetail
from fides.api.models.comment import Comment, CommentReference
from fides.api.models.connection_config_saas_history import ConnectionConfigSaaSHistory
from fides.api.models.connection_oauth_credentials import OAuthConfig
from fides.api.models.connectionconfig import ConnectionConfig
from fides.api.models.consent_automation import ConsentAutomation
Expand Down Expand Up @@ -91,6 +92,7 @@
Property,
)
from fides.api.models.questionnaire import ChatMessage, Questionnaire
from fides.api.models.saas_config_version import SaaSConfigVersion
from fides.api.models.saas_template_dataset import SaasTemplateDataset
from fides.api.models.storage import StorageConfig
from fides.api.models.system_compass_sync import SystemCompassSync
Expand Down
43 changes: 43 additions & 0 deletions src/fides/api/db/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from fides.api.models.fides_user import FidesUser
from fides.api.models.fides_user_permissions import FidesUserPermissions
from fides.api.models.policy import Policy, Rule, RuleTarget
from fides.api.models.saas_config_version import SaaSConfigVersion
from fides.api.models.sql_models import ( # type: ignore[attr-defined]
Dataset,
Organization,
Expand Down Expand Up @@ -322,6 +323,47 @@ def load_default_dsr_policies(session: Session) -> None:
log.info("All default policies & rules created")


def sync_oob_saas_config_versions(session: Session) -> None:
"""
Upserts a SaaSConfigVersion row for every bundled (OOB) SaaS connector
template currently loaded in memory.

Called on startup so the table is bootstrapped on first deploy and
picks up new template versions automatically on each upgrade.
Rows are immutable once written, so this is safe to call repeatedly.
"""
# Import here to avoid circular imports at module load time
from fides.api.schemas.saas.saas_config import (
SaaSConfig, # pylint: disable=import-outside-toplevel
)
from fides.api.service.connectors.saas.connector_registry_service import ( # pylint: disable=import-outside-toplevel
FileConnectorTemplateLoader,
)
from fides.api.util.saas_util import ( # pylint: disable=import-outside-toplevel
load_config_from_string,
load_dataset_from_string,
)

templates = FileConnectorTemplateLoader.get_connector_templates()
for connector_type, template in templates.items():
try:
saas_config = SaaSConfig(**load_config_from_string(template.config))
dataset = load_dataset_from_string(template.dataset)
SaaSConfigVersion.upsert(
db=session,
connector_type=connector_type,
version=saas_config.version,
config=saas_config.model_dump(mode="json"),
dataset=dataset,
is_custom=False,
)
except Exception: # pylint: disable=broad-except
log.exception(
"Failed to sync SaaSConfigVersion for OOB connector '{}'",
connector_type,
)


def load_default_resources(session: Session) -> None:
"""
Seed the database with default resources that the application
Expand All @@ -330,6 +372,7 @@ def load_default_resources(session: Session) -> None:
load_default_organization(session)
load_default_taxonomy(session)
load_default_dsr_policies(session)
sync_oob_saas_config_versions(session)


async def load_samples(async_session: AsyncSession) -> None:
Expand Down
71 changes: 71 additions & 0 deletions src/fides/api/models/connection_config_saas_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from typing import Any, Dict, List, Optional

from sqlalchemy import Column, ForeignKey, String
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import Session

from fides.api.db.base_class import Base


class ConnectionConfigSaaSHistory(Base):
"""
Append-only snapshot of a connection's SaaS config taken each time
ConnectionConfig.update_saas_config() is called.

Unlike SaaSConfigVersion (which stores one row per connector_type/version),
this table is scoped to an individual ConnectionConfig instance. It captures
divergent configs — e.g. when a connection is individually PATCHed to a
version that differs from the shared template.

connection_key is denormalized so that history is still queryable if the
parent ConnectionConfig row is deleted (FK uses ON DELETE SET NULL).
"""

@declared_attr
def __tablename__(self) -> str:
return "connection_config_saas_history"

connection_config_id = Column(
String,
ForeignKey("connectionconfig.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
connection_key = Column(String, nullable=False)
version = Column(String, nullable=False)
config = Column(JSONB, nullable=False)
dataset = Column(JSONB, nullable=True)

def __repr__(self) -> str:
return (
f"<ConnectionConfigSaaSHistory("
f"connection_key='{self.connection_key}', "
f"version='{self.version}', "
f"id='{self.id}')>"
)

@classmethod
def create_snapshot(
cls,
db: Session,
connection_config_id: str,
connection_key: str,
version: str,
config: Dict[str, Any],
datasets: Optional[List[Dict[str, Any]]] = None,
) -> "ConnectionConfigSaaSHistory":
"""
Appends a new history row. Always creates a new row — no upsert logic —
so every write is preserved as a distinct audit entry.
"""
return cls.create(
db=db,
data={
"connection_config_id": connection_config_id,
"connection_key": connection_key,
"version": version,
"config": config,
"dataset": datasets or None,
},
)
22 changes: 22 additions & 0 deletions src/fides/api/models/connectionconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,11 @@ def update_saas_config(
Updates the SaaS config and initializes any empty secrets with
connector param default values if available (will not override any existing secrets)
"""
from fides.api.models.connection_config_saas_history import (
ConnectionConfigSaaSHistory,
)
from fides.api.models.datasetconfig import DatasetConfig

default_secrets = {
connector_param.name: connector_param.default_value
for connector_param in saas_config.connector_params
Expand All @@ -366,6 +371,23 @@ def update_saas_config(
updated_secrets = {**default_secrets, **(self.secrets or {})}
self.secrets = updated_secrets
self.saas_config = saas_config.model_dump(mode="json")

datasets = [
dc.ctl_dataset.dict()
for dc in db.query(DatasetConfig)
.filter(DatasetConfig.connection_config_id == self.id)
.all()
if dc.ctl_dataset
]
ConnectionConfigSaaSHistory.create_snapshot(
db=db,
connection_config_id=self.id,
connection_key=self.key,
version=saas_config.version,
config=self.saas_config,
datasets=datasets if datasets else None,
)

self.save(db)

def update_test_status(
Expand Down
Loading
Loading