fix: give the remaining asset foreign keys a delete rule
Migration 040 fixed the two constraints pointing at vulnerabilities, which was
only half the chain — I checked what referenced findings and not what
referenced assets. So the delete got one step further and failed on the next
constraint instead:
ForeignKeyViolation: update or delete on table "assets" violates foreign
key constraint "notification_logs_asset_id_fkey"
This time every foreign key into the delete path was enumerated. Three had no
rule, each gets the one that fits: the notification log keeps its row with the
reference cleared (it records that a mail WAS sent, which stays true), while
scans and findings belong to their asset, are NOT NULL, and go with it. The
asset_groups association is included even though SQLAlchemy clears it itself —
the database should not depend on the application getting the order right.
Verified no foreign key into assets, vulnerabilities or scans is left without
a rule.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""Give the remaining asset FKs a delete rule.
|
||||
|
||||
Migration 040 fixed the two constraints pointing at vulnerabilities, which was
|
||||
only half the chain. Deleting an asset then got one step further and failed on
|
||||
the next one:
|
||||
|
||||
ForeignKeyViolation: update or delete on table "assets" violates foreign
|
||||
key constraint "notification_logs_asset_id_fkey"
|
||||
|
||||
Three tables reference assets with no ON DELETE rule. Each gets the rule that
|
||||
fits it: a notification log records that a mail WAS sent and stays (reference
|
||||
cleared, column is already nullable); a scan and a finding belong to their
|
||||
asset, cannot outlive it, and are NOT NULL, so they go with it.
|
||||
|
||||
Revision ID: 041
|
||||
Revises: 040
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "041"
|
||||
down_revision = "040"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# (constraint, table, ondelete)
|
||||
_FKS = [
|
||||
("notification_logs_asset_id_fkey", "notification_logs", "SET NULL"),
|
||||
("scans_asset_id_fkey", "scans", "CASCADE"),
|
||||
("vulnerabilities_asset_id_fkey", "vulnerabilities", "CASCADE"),
|
||||
# SQLAlchemy clears the association rows itself, so this one never failed —
|
||||
# included so the database does not rely on it.
|
||||
("asset_groups_asset_id_fkey", "asset_groups", "CASCADE"),
|
||||
]
|
||||
|
||||
|
||||
def upgrade():
|
||||
for name, table, rule in _FKS:
|
||||
# The constraint name is the PostgreSQL default; tolerate a database
|
||||
# where it was created differently rather than aborting the upgrade.
|
||||
try:
|
||||
op.drop_constraint(name, table, type_="foreignkey")
|
||||
except Exception:
|
||||
continue
|
||||
op.create_foreign_key(name, table, "assets", ["asset_id"], ["id"],
|
||||
ondelete=rule)
|
||||
|
||||
|
||||
def downgrade():
|
||||
for name, table, _rule in _FKS:
|
||||
try:
|
||||
op.drop_constraint(name, table, type_="foreignkey")
|
||||
except Exception:
|
||||
continue
|
||||
op.create_foreign_key(name, table, "assets", ["asset_id"], ["id"])
|
||||
+4
-1
@@ -17,7 +17,10 @@ user_groups = Table(
|
||||
asset_groups = Table(
|
||||
'asset_groups',
|
||||
Base.metadata,
|
||||
Column('asset_id', Integer, ForeignKey('assets.id'), primary_key=True),
|
||||
# CASCADE: SQLAlchemy clears these rows itself on delete, but the database
|
||||
# should not depend on the application doing it in the right order.
|
||||
Column('asset_id', Integer, ForeignKey('assets.id', ondelete='CASCADE'),
|
||||
primary_key=True),
|
||||
Column('group_id', Integer, ForeignKey('groups.id'), primary_key=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -34,7 +34,12 @@ class NotificationLog(Base):
|
||||
# (ForeignKeyViolation on notification_logs_vulnerability_id_fkey).
|
||||
vulnerability_id = Column(Integer, ForeignKey("vulnerabilities.id", ondelete="SET NULL"),
|
||||
nullable=True, index=True)
|
||||
asset_id = Column(Integer, ForeignKey("assets.id"), nullable=True, index=True)
|
||||
# SET NULL for the same reason as vulnerability_id above: the log records
|
||||
# that a notification WAS sent, which stays true after the asset is gone.
|
||||
# Without a rule this blocked asset deletion outright — the vulnerability_id
|
||||
# fix in migration 040 only moved the failure one constraint along.
|
||||
asset_id = Column(Integer, ForeignKey("assets.id", ondelete="SET NULL"),
|
||||
nullable=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
|
||||
notification_type = Column(SQLEnum(NotificationType), nullable=False, index=True)
|
||||
sent_at = Column(DateTime, nullable=False, index=True)
|
||||
|
||||
+3
-1
@@ -32,7 +32,9 @@ class Scan(Base, TimestampMixin):
|
||||
__tablename__ = "scans"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
asset_id = Column(Integer, ForeignKey("assets.id"), nullable=False, index=True)
|
||||
# CASCADE: a scan belongs to its asset and cannot outlive it (NOT NULL).
|
||||
asset_id = Column(Integer, ForeignKey("assets.id", ondelete="CASCADE"),
|
||||
nullable=False, index=True)
|
||||
|
||||
# Scan-Details
|
||||
scan_type = Column(
|
||||
|
||||
@@ -47,7 +47,10 @@ class Vulnerability(Base, TimestampMixin):
|
||||
# non-CVE findings (EOL, compliance, weak ciphers) are imported with a
|
||||
# pseudo-CVE id of the form 'NESSUS-PLUGIN-{plugin_id}'.
|
||||
cve_id = Column(String(50), nullable=False, index=True) # CVE-2024-1234 or NESSUS-PLUGIN-12345
|
||||
asset_id = Column(Integer, ForeignKey("assets.id"), nullable=False, index=True)
|
||||
# CASCADE: the ORM already cascades, but the database should not depend on
|
||||
# the application getting the order right.
|
||||
asset_id = Column(Integer, ForeignKey("assets.id", ondelete="CASCADE"),
|
||||
nullable=False, index=True)
|
||||
|
||||
# CVSS-Bewertung
|
||||
cvss_score = Column(Float, nullable=True, index=True)
|
||||
|
||||
Reference in New Issue
Block a user