Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
118 changes: 62 additions & 56 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,51 +11,56 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11']
django-version: ['3.2', '4.0', '4.1', '4.2']
python-version: ['3.9', '3.10', '3.11', '3.12']
django-version: ['3.2', '4.1', '4.2', '5.0']
exclude:
- python-version: '3.8'
django-version: '4.0'
- python-version: '3.8'
django-version: '4.1'
- python-version: '3.8'
django-version: '4.2'

services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
# Django 5.0 requires Python 3.10+
- python-version: '3.9'
django-version: '5.0'

# We'll use a custom setup approach instead of services

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install PostgreSQL Anonymizer Extension
- name: Setup PostgreSQL with Anonymizer Extension
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client
# Install postgresql_anonymizer extension
sudo apt-get install -y postgresql-13-postgresql-anonymizer || echo "Extension not available in apt, will build from source"
# Pull the official PostgreSQL Anonymizer Docker image
docker pull registry.gitlab.com/dalibo/postgresql_anonymizer:stable

# Run the PostgreSQL container with anonymizer extension
docker run -d \
--name postgres-anon \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_DB=test_db \
-p 5432:5432 \
registry.gitlab.com/dalibo/postgresql_anonymizer:stable

# Wait for PostgreSQL to be ready (with timeout)
echo "Waiting for PostgreSQL to be ready..."
for i in {1..30}; do
if docker exec postgres-anon pg_isready -U postgres; then
echo "PostgreSQL is ready!"
break
fi
echo "Attempt $i/30: PostgreSQL not ready yet, waiting..."
sleep 2
done

# Final check
docker exec postgres-anon pg_isready -U postgres

- name: Cache pip dependencies
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}-${{ hashFiles('**/setup.py') }}
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-

Expand All @@ -69,20 +74,27 @@ jobs:

- name: Setup test database
run: |
export PGPASSWORD=postgres
psql -h localhost -U postgres -d test_db -c "CREATE EXTENSION IF NOT EXISTS anon CASCADE;"
# Install PostgreSQL client for local psql commands
sudo apt-get update && sudo apt-get install -y postgresql-client

# Create the anonymizer extension
PGPASSWORD=postgres psql -h localhost -U postgres -d test_db -c "CREATE EXTENSION IF NOT EXISTS anon CASCADE;"

# Verify extension was created
PGPASSWORD=postgres psql -h localhost -U postgres -d test_db -c "SELECT name, installed_version FROM pg_available_extensions WHERE name = 'anon';"

- name: Run tests
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
DJANGO_SETTINGS_MODULE: tests.settings
run: |
coverage run --source='.' -m pytest tests/ -v
coverage report
coverage xml
# Run all tests with PostgreSQL anonymizer extension available
pytest tests/ -v --cov=django_postgres_anon --cov-report=xml --cov-report=term-missing --cov-fail-under=87

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
Expand All @@ -91,45 +103,39 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 black isort mypy
pip install -e .
pip install -e ".[dev]"

- name: Run black
run: black --check .
- name: Run ruff format check (replaces black)
run: ruff format --check .

- name: Run isort
run: isort --check-only .

- name: Run flake8
run: flake8 django_postgres_anon tests

- name: Run mypy
run: mypy django_postgres_anon --ignore-missing-imports
- name: Run ruff lint check (replaces flake8, isort, etc.)
run: ruff check django_postgres_anon tests

security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install bandit safety
pip install -e .

- name: Run bandit
run: bandit -r django_postgres_anon/
Expand Down
3 changes: 1 addition & 2 deletions django_postgres_anon/admin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from typing import ClassVar, Optional

import yaml
from django.contrib import admin, messages
from django.db.models import QuerySet
from django.http import HttpRequest, HttpResponse
from django.utils.html import format_html

import yaml

from django_postgres_anon.admin_base import BaseAnonymizationAdmin, BaseLogAdmin

# Removed constants - using inline values
Expand Down
2 changes: 1 addition & 1 deletion django_postgres_anon/management/commands/anon_drop.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,6 @@ def _print_summary(self, removed_labels, removed_rules, removed_presets, removed
self.stdout.write(f" • {error}")

if not any([removed_labels, removed_rules, removed_presets, removed_roles]):
self.stdout.write(self.style.WARNING("ℹ️ Nothing to remove"))
self.stdout.write(self.style.WARNING("Nothing to remove"))
elif not options["dry_run"]:
self.stdout.write(self.style.SUCCESS("\n🧹 Anonymization removal completed!"))
8 changes: 4 additions & 4 deletions django_postgres_anon/management/commands/anon_dump.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
import subprocess
import subprocess # nosec B404

from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
Expand Down Expand Up @@ -78,10 +78,10 @@ def handle(self, *args, **options):
try:
# Test if --exclude-extension option is available
test_cmd = ["pg_dump", "--help"]
result = subprocess.run(test_cmd, capture_output=True, text=True, check=False)
result = subprocess.run(test_cmd, capture_output=True, text=True, check=False) # nosec B603
if "--exclude-extension" in result.stdout:
cmd.append("--exclude-extension=anon")
except Exception:
except Exception: # nosec B110
# If we can't test, just skip the exclude option
pass

Expand All @@ -102,7 +102,7 @@ def handle(self, *args, **options):
self.stdout.write(f"Creating anonymized dump using masked role '{masked_role}'...")
self.stdout.write(f"Output file: {options['output_file']}")

result = subprocess.run(cmd, env=env, capture_output=True, text=True, check=False)
result = subprocess.run(cmd, env=env, capture_output=True, text=True, check=False) # nosec B603

if result.returncode == 0:
self.stdout.write(self.style.SUCCESS(f"✅ Anonymized dump created: {options['output_file']}"))
Expand Down
3 changes: 1 addition & 2 deletions django_postgres_anon/management/commands/anon_load_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import os
from pathlib import Path

from django.core.management.base import BaseCommand, CommandError

import yaml
from django.core.management.base import BaseCommand, CommandError

from django_postgres_anon.models import MaskingPreset, MaskingRule
from django_postgres_anon.utils import create_operation_log, validate_function_syntax
Expand Down
18 changes: 6 additions & 12 deletions django_postgres_anon/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import logging
import os
from typing import Optional

from django.db import models
from django.utils import timezone

import yaml
from django.db import connection, models
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from django.utils import timezone


class MaskingRule(models.Model):
Expand Down Expand Up @@ -107,7 +109,7 @@ def load_from_yaml(cls, yaml_path: str, preset_name: Optional[str] = None) -> "M
if not preset_name:
preset_name = os.path.splitext(os.path.basename(yaml_path))[0]

preset, created = cls.objects.get_or_create(
preset, _created = cls.objects.get_or_create(
name=preset_name, defaults={"description": f"Loaded from {yaml_path}"}
)

Expand Down Expand Up @@ -158,14 +160,6 @@ def __str__(self) -> str:
return f"{status} {self.get_operation_display()} - {self.timestamp}"


import logging

from django.db import connection

# Signal handlers for automatic security label management
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver

logger = logging.getLogger(__name__)


Expand Down
2 changes: 0 additions & 2 deletions django_postgres_anon/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

from django_postgres_anon.constants import DEFAULT_POSTGRES_PORT

# from django_postgres_anon.exceptions import AnonDatabaseError, AnonValidationError

logger = logging.getLogger(__name__)


Expand Down
20 changes: 13 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -286,19 +286,25 @@ select = [
"PERF", # performance anti-patterns
]
ignore = [
"E501", # line too long (handled by formatter)
"B008", # do not perform function calls in argument defaults
"B904", # raise from
"S101", # assert used (needed for tests)
"S105", # hardcoded password (false positives in tests)
"S106", # hardcoded password (false positives in tests)
"SIM105", # contextlib.suppress
"E501", # line too long (handled by formatter)
"B008", # do not perform function calls in argument defaults
"B904", # raise from
"S101", # assert used (needed for tests)
"S105", # hardcoded password (false positives in tests)
"S106", # hardcoded password (false positives in tests)
"SIM105", # contextlib.suppress
"PLR0913", # too many arguments
"PLR2004", # magic value comparison
"PLR0912", # too many branches
"PERF203", # try-except in loop
"SIM102", # use single if statement
"SIM103", # return condition directly
"PLC0415", # import at top-level (needed for lazy imports)
"S603", # subprocess call checks (we control the inputs)
"S110", # try-except-pass (sometimes needed for optional features)
"RUF012", # mutable class attributes (Django Meta classes)
"RUF043", # regex patterns in tests are fine
"SIM117", # nested with statements (sometimes clearer)
]

[tool.ruff.lint.per-file-ignores]
Expand Down
4 changes: 0 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ def django_db_setup(django_db_setup):
def user():
"""Create a regular test user"""
from django.contrib.auth.models import User

from model_bakery import baker

return baker.make(User, email="[email protected]")
Expand All @@ -48,7 +47,6 @@ def user():
def admin_user():
"""Create an admin user"""
from django.contrib.auth.models import User

from model_bakery import baker

return baker.make(User, is_superuser=True, is_staff=True, email="[email protected]")
Expand All @@ -58,7 +56,6 @@ def admin_user():
def masked_user():
"""Create a user in the masked data group"""
from django.contrib.auth.models import Group, User

from model_bakery import baker

user = baker.make(User, email="[email protected]")
Expand All @@ -71,7 +68,6 @@ def masked_user():
def staff_user():
"""Create a staff user"""
from django.contrib.auth.models import User

from model_bakery import baker

return baker.make(User, is_staff=True, email="[email protected]")
Expand Down
Loading