Problem 1: Remove legacy role bypass
- Remove role="admin" string bypass in permissions.py resolve_permissions()
- Remove role="admin"/"editor" bypass in auth.py check_permission()
- Remove legacy role string fallback in deps.py require_admin/require_write
- Add migration 0112: Create Role records for built-in roles and link role_id
- KI-Kommentar: Legacy Role Bypass entfernt — alle Admins müssen echte role_id haben
Problem 2: Enforce API token scopes
- Add _token_scopes check in require_permission() in deps.py
- When _token_scopes is set (API token auth), required permission must be in scopes
- When _token_scopes not set (session auth), normal permission check applies
Problem 3: Migration chain verification
- Chain is already linear: 0027→0028_rls_force→0028_user_preferences→0029
- user_preferences table confirmed exists in DB
- No duplicate revision IDs found
Problem 4: RLS for remaining tenant tables
- Add migration 0111: Dynamic RLS activation for any remaining tables with tenant_id
- Login tables and global tables explicitly excluded
- DB check shows 0 tables currently missing RLS (safety net migration)
Problem 5: Permission cache invalidation on tenant switch
- Add invalidate_permission_cache() call in switch_tenant() for old tenant
- Stale cached permissions from old tenant no longer leak
Problem 6+7: Guest system removal
- Remove get_current_guest() from deps.py
- Remove guest_auth.py router from main.py and routes/__init__.py
- Rewrite guests.py to use regular User/UserTenant with role=guest
- Remove GuestUser/GuestInvitation from models/__init__.py
- Add migration 0113: Migrate guest_users to regular users, drop guest tables
- Update frontend GuestLogin/GuestContacts to redirect to normal pages
- KI-Kommentar: Guest-System umgebaut — Guests sind jetzt reguläre User mit role=guest
- Add tests/test_permission_system_live.py: 33 live tests against real PostgreSQL
testing RBAC, ABAC, RLS, cross-tenant isolation, guest access, entity sharing,
field-level permissions, role invalidation, group permissions, membership suspension
- fix(contacts): delete route uses contacts:delete instead of contacts:write
The delete_contact and delete_contact_person routes were checking contacts:write
permission instead of contacts:delete, allowing users without delete permission
to delete contacts.
- fix(contacts): DeleteContactCommand passes is_system_admin to service
DeleteContactCommand.run() was not passing is_system_admin from the session
to contact_service.delete_contact(), causing system admins to be blocked
by the row-level admin access check.
- fix(contacts): allow deletion of tenant-owned contacts
contact_service.delete_contact() required admin-level entity access for ALL
contacts, including tenant-owned ones (owner_id=None). Tenant-owned contacts
can now be deleted by any user with contacts:delete permission (already
verified by the route via require_permission).
Plugin route registration in main.py was mutating module-level router
singletons by appending require_active_plugin dependencies directly to
router.routes. This persisted across app instances, causing test routes
to inherit require_active_plugin checks and return 403 "plugin inactive"
when tests created their own FastAPI apps with those routers.
Fix: use app.include_router(router, dependencies=[plugin_dep]) which
adds dependencies at the app level without modifying the shared router.
Fixes 35 test failures across 4 test files:
- test_agent_memory.py (6 failures)
- test_external_agent_api.py (15 failures)
- test_graph_rag.py (7 failures)
- test_marketplace.py (7 failures)
- Use migration engine (crm_migration, BYPASSRLS) for default data seeding
in app/main.py instead of crm_api role which is RLS-enforced
- Skip domains PATCH for dockercompose apps in deploy_api() to avoid 422
- Regenerate migration_hashes.txt for 0085_restore_tenant_rls.py
- Add migration 0110: password_salt column to mail_accounts
LOGIN_TABLES (users, user_tenants, tenants, sessions, password_reset_tokens)
added to skip list. RLS on these tables blocked crm_auth from reading users
during login → 401 Invalid email or password.
crm_auth grants applied directly (no RLS) matching 0085 AUTH_TABLES.
⚠️ LOGIN-TABELLEN DÜRFEN KEIN RLS BEKOMMEN — RLS blockiert crm_auth beim Login.
Siehe 0085 AUTH_TABLES für die korrekten Grants.
Plugin migrations run after core migration 0085 which sets up RLS for
all known core tables. Plugin-created tables were left without RLS,
creating a critical multi-tenant isolation gap (84 tables affected).
The migration runner now automatically enables RLS on all newly created
tenant tables after validation:
- ENABLE + FORCE ROW LEVEL SECURITY
- Idempotent DROP IF EXISTS + CREATE fail-closed tenant isolation policy
- GRANT CRUD to crm_api and crm_worker
- ALTER TABLE OWNER TO crm_migration
Global tables (-- GLOBAL TABLE comment) are skipped.
Plugin activation was broken for ALL inactive plugins because
sync_notification_types() tried to INSERT search_reindex_complete (22 chars)
into type_key VARCHAR(20), causing StringDataRightTruncationError.
Alembic head: 0106 → 0107
unified_search plugin activation calls sync_notification_types() which
DELETEs stale rows from notification_types. App DB user (crm_api) lacked
DELETE permission, causing plugin activation to fail with
InsufficientPrivilegeError.
Alembic head: 0105 → 0106
Fixes 3 issues found by API integration tests:
1. migration_runner.py: Add GLOBAL TABLE exemption for tables without tenant_id
- New _extract_global_table_names() method parses -- GLOBAL TABLE: comments
- marketplace_listings is intentionally global (no tenant_id)
2. unified_search/migrations/0002_embeddings.sql: Remove companies table (does not exist),
add DO $$ BEGIN END $$ blocks to check table existence before ALTER
3. marketplace/migrations/0001_initial.sql: Add -- GLOBAL TABLE: marketplace_listings comment
Two critical bugs found by API integration tests:
1. contacts.embedding (vector(768)) — ORM model updated in Phase 5.3 but
plugin migration 0002_embeddings.sql was never run as Alembic migration.
Also adds embedding columns to mails, companies, files, calendar_entries, tags.
2. audit_log.created_at, updated_at, deleted_at — AuditLog inherits TenantMixin
which expects these columns, but they were never added to the DB table.
Also adds to deletion_log.
Migration uses IF NOT EXISTS checks for all columns/indexes.
Alembic head: 0103 → 0104
The CircuitBreakerMiddleware was blocking all requests (503 circuit_open) because
every exception in get_db() — including 401 Unauthorized, 403 Forbidden, 404 Not Found —
was calling record_failure() on the DB circuit breaker. This caused the circuit to
trip after 5 non-DB errors (e.g. failed login attempts during security testing).
Fix: Only call record_failure() when _is_transient_db_error(exc) returns True,
filtering out HTTP exceptions that are not DB-related.
- app/core/resilience.py: CircuitBreaker (CLOSED/OPEN/HALF_OPEN), retry_db,
redis_call_with_fallback, InMemoryRateLimiter, CircuitBreakerMiddleware
- app/core/auth.py: get_session_data now falls back to PostgreSQL sessions
table when Redis is unavailable
- app/core/permissions.py: get_cached_permissions falls back to direct DB
resolution when Redis circuit is open
- app/core/rate_limit.py: check_rate_limit falls back to in-memory limiter
when Redis is down; reset_rate_limit clears both Redis and in-memory
- app/core/middleware.py: CSRF validation uses get_session_data (Redis+DB
fallback); sliding session TTL is best-effort during outage
- app/core/db/__init__.py: get_db() wraps session creation with retry_db
for transient connection errors; records circuit breaker success/failure
- app/deps.py: refresh_session_ttl wrapped in try/except for Redis outage
- app/main.py: CircuitBreakerMiddleware registered (returns 503 when DB
circuit is OPEN, skips health/metrics endpoints)
- app/config.py: Added resilience settings (thresholds, cooldown, retries)
- tests/test_resilience.py: 30 tests covering all patterns
30/30 resilience tests pass. No regressions in plugin lifecycle tests.
- .env.example: Add ADMIN_EMAIL/ADMIN_PASSWORD
- SANIERUNGS_FORTSCHRITT.md: Update UUIDs
- IMPLEMENTATION_PLAN.md: Update UUID
- promptinclude: Worker/DB/Redis are now part of Docker-Compose-App
- Replace old UUID stvabl4vaqru7jclx4ittzr3 with dx4pqdziu4uj6x9fxs1u5z0x
- Remove old worker UUID asxqaq3566to108xordck0ff
- Update INSTALL.md: Stand 2026-08-04, Commit 0ebc411, Alembic-Head 0103
- Update DEPLOY.md: New UUID and auto-resolve via APP_DOMAIN
- prestart.sh: runs seed_admin.py after migrations
- seed_admin.py: reads ADMIN_EMAIL and ADMIN_PASSWORD from env vars
- Creates default tenant + admin role + admin user if not exists
- No custom crm-net network — Coolify manages networking
- No custom Traefik labels — Coolify generates them
- No hardcoded domains — all from environment variables
- Simplified volumes — no custom names
- deploy.py: UUIDs from env vars or Coolify API lookup by name
- fast-deploy.sh: No hardcoded UUIDs, APP_DOMAIN from env
- docker-compose.yml: All secrets from env vars, no hardcoded values
- .env.example: All required vars documented
- Deleted obsolete fast-frontend-deploy.sh with hardcoded container name