Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unique constraint to database plugin names #3556

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
10 changes: 9 additions & 1 deletion boefjes/boefjes/katalogus/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from boefjes.katalogus import organisations, plugins
from boefjes.katalogus import settings as settings_router
from boefjes.katalogus.version import __version__
from boefjes.storage.interfaces import NotAllowed, NotFound, StorageError
from boefjes.storage.interfaces import IntegrityError, NotAllowed, NotFound, StorageError

with settings.log_cfg.open() as f:
logging.config.dictConfig(json.load(f))
Expand Down Expand Up @@ -89,6 +89,14 @@ def not_allowed_handler(request: Request, exc: NotAllowed):
)


@app.exception_handler(IntegrityError)
def integrity_error_handler(request: Request, exc: IntegrityError):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"message": exc.message},
)


@app.exception_handler(StorageError)
def storage_error_handler(request: Request, exc: StorageError):
return JSONResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Unique plugin names

Revision ID: a2c8d54b0124
Revises: 870fc302b852
Create Date: 2024-09-18 14:46:00.881022

"""

from alembic import op

# revision identifiers, used by Alembic.
revision = "a2c8d54b0124"
down_revision = "870fc302b852"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint("unique_boefje_name", "boefje", ["name"])
op.create_unique_constraint("unique_normalizer_name", "normalizer", ["name"])
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("unique_normalizer_name", "normalizer", type_="unique")
op.drop_constraint("unique_boefje_name", "boefje", type_="unique")
# ### end Alembic commands ###
4 changes: 2 additions & 2 deletions boefjes/boefjes/sql/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class BoefjeInDB(SQL_BASE):
static = Column(Boolean, nullable=False, server_default="false")

# Metadata
name = Column(String(length=64), nullable=False)
name = Column(String(length=64), nullable=False, unique=True)
description = Column(types.Text, nullable=True)
scan_level = Column(types.Enum(*[str(x.value) for x in ScanLevel], name="scan_level"), nullable=False, default="4")

Expand All @@ -92,7 +92,7 @@ class NormalizerInDB(SQL_BASE):
static = Column(Boolean, nullable=False, server_default="false")

# Metadata
name = Column(String(length=64), nullable=False)
name = Column(String(length=64), nullable=False, unique=True)
description = Column(types.Text, nullable=True)

# Job specifications
Expand Down
16 changes: 7 additions & 9 deletions boefjes/boefjes/sql/session.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import structlog
from sqlalchemy.exc import DatabaseError
from sqlalchemy import exc
from sqlalchemy.orm import Session
from typing_extensions import Self

from boefjes.storage.interfaces import StorageError
from boefjes.storage.interfaces import IntegrityError, StorageError

logger = structlog.get_logger(__name__)

Expand Down Expand Up @@ -34,15 +34,13 @@ def __exit__(self, exc_type: type[Exception], exc_value: str, exc_traceback: str

return

error = None

try:
logger.debug("Committing session")
self.session.commit()
except DatabaseError as e:
error = e
except exc.IntegrityError as e:
Donnype marked this conversation as resolved.
Show resolved Hide resolved
raise IntegrityError("A storage error occurred") from e
except exc.DatabaseError as e:
raise StorageError("A storage error occurred") from e
finally:
logger.exception("Committing failed, rolling back")
self.session.rollback()

if error:
raise StorageError("A storage error occurred") from error
7 changes: 7 additions & 0 deletions boefjes/boefjes/storage/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ def __init__(self, message: str):
self.message = message


class IntegrityError(StorageError):
"""Integrity error during persistence of an entity"""

def __init__(self, message: str):
self.message = message


class SettingsNotConformingToSchema(StorageError):
def __init__(self, plugin_id: str, validation_error: str):
super().__init__(f"Settings for plugin {plugin_id} are not conform the plugin schema: {validation_error}")
Expand Down
18 changes: 18 additions & 0 deletions boefjes/tests/integration/test_api.py
Donnype marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ def test_add_boefje(test_client, organisation):
assert response.json() == boefje_dict


def test_cannot_add_plugin_with_duplicate_name(test_client, organisation):
boefje = Boefje(id="test_plugin", name="My test boefje", static=False)
response = test_client.post(f"/v1/organisations/{organisation.id}/plugins", content=boefje.json())
assert response.status_code == 201

boefje = Boefje(id="test_plugin_2", name="My test boefje", static=False)
response = test_client.post(f"/v1/organisations/{organisation.id}/plugins", content=boefje.json())
assert response.status_code == 400

normalizer = Normalizer(id="test_normalizer", name="My test normalizer", static=False)
response = test_client.post(f"/v1/organisations/{organisation.id}/plugins", content=normalizer.json())
assert response.status_code == 201

normalizer = Normalizer(id="test_normalizer_2", name="My test normalizer", static=False)
response = test_client.post(f"/v1/organisations/{organisation.id}/plugins", content=normalizer.json())
assert response.status_code == 400


def test_delete_boefje(test_client, organisation):
boefje = Boefje(id="test_plugin", name="My test boefje", static=False)
response = test_client.post(f"/v1/organisations/{organisation.id}/plugins", content=boefje.json())
Expand Down