Skip to main content

Reconciliation audit for Alliance Auth's Discord integration.

Project description

aa-discord-audit

PyPI version Supported Python versions License: MIT

Reconciliation audit for Alliance Auth's Discord integration. Compares the actual role assignments in the configured Discord guild against the state expressed in Alliance Auth (Groups + State per user) and closes the gap through which moderators can hand-assign AA-named roles to users AA does not know about.

Status: alpha (0.1.x). The public API and settings may still change before 1.0.

Contents

Safety posture

The audit is safe by default:

  • The first run after install is locked to dry-run regardless of operator settings. Releasing the lock requires an explicit InitialAuditAcknowledgement (admin or shell only).
  • The default policy is report for every category — destructive actions are opt-in.
  • Audit-trail rows (AuditRun, AuditFinding, AuditInvocation, ConfigChangeLog) are append-only at the manager and instance layers; bulk update() / bulk_update() are blocked.
  • Webhook URLs are treated as credentials and redacted from logs, exception messages, and persisted argv.

How it works

Each guild member is classified into one category:

Category Meaning
unknown_guest Discord member AA knows nothing about
linked_no_perm Identity known to AA but lacks discord.access_discord
bot_filtered Configured bot account — never acted upon

The operator maps each category to one action:

Action Behaviour
report Record the finding; no Discord-side change
strip Remove AA-managed roles
strip_kick Remove AA-managed roles, then kick from guild

The mapping is the AA_DISCORD_AUDIT_POLICY setting; per-group and per-state overrides nest inside each category.

Requirements

  • Python 3.10–3.13
  • Django 4.2 or 5.2
  • Alliance Auth 4.x or 5.x
  • Alliance Auth's Discord service module (allianceauth.services.modules.discord) installed and configured (bot token + guild)

Installation

pip install aa-discord-audit

In your Auth local.py:

# `aa_discord_audit` must appear AFTER
# `allianceauth.services.modules.discord` so the discord module's
# models load first; `apps.ready()` raises `ImproperlyConfigured`
# otherwise.
INSTALLED_APPS += ["aa_discord_audit"]

MIDDLEWARE += [
    "aa_discord_audit.current_user.CurrentUserMiddleware",
]

Then run migrations:

python manage.py migrate aa_discord_audit

The CurrentUserMiddleware is mandatory — apps.ready() raises ImproperlyConfigured if it is missing. It is what lets the ConfigChangeLog signal handler attribute admin edits to a real user instead of <system>.

Quick start

  1. Grant aa_discord_audit.run_audit to the operator role that runs audits.

  2. Run a dry-run audit:

    python manage.py audit_discord_roles --action report
    
  3. Review findings under Discord Audit → Audit runs in the Auth dashboard.

  4. Release the first-run lock — either create an InitialAuditAcknowledgement row through the admin, or run python manage.py audit_acknowledge_initial. Both require aa_discord_audit.run_audit and aa_discord_audit.acknowledge_initial_audit.

  5. Re-run with the destructive action of your choice when ready.

Permissions

Codename Gates
aa_discord_audit.run_audit management command, beat task, run delete
aa_discord_audit.run_audit_destructive web-launch gate for strip / strip_kick (separate from run_audit)
aa_discord_audit.acknowledge_initial_audit release the first-run dry-run lock
aa_discord_audit.manage_discord_identity DiscordIdentity admin
aa_discord_audit.manage_role_exception ManagedRoleException admin
aa_discord_audit.manage_protected_member ProtectedDiscordMember admin
aa_discord_audit.manage_bot_account_uid BotAccountUid admin
aa_discord_audit.manage_finding_override FindingActionOverride admin
aa_discord_audit.view_auditrun (and friends) read-only audit-trail in the Auth dashboard

The manage_* codenames are split per blast radius so a junior with manage_bot_account_uid cannot also defang the audit by editing ManagedRoleException.

Settings

All settings are optional. Defaults are safe.

# Action policy. Bare-string form below is shorthand for
# {"default": "<action>"}; use the nested form for per-group / per-state
# overrides keyed by AA group name and state name.
AA_DISCORD_AUDIT_POLICY = {
    "unknown_guest":  "report",
    "linked_no_perm": "report",
    # "linked_no_perm": {
    #     "default":  "strip",
    #     "by_state": {"Guest": "report"},
    #     "by_group": {"Directors": "report"},
    # },
}

# AA-notify fan-out to permission holders.
AA_DISCORD_AUDIT_NOTIFY_ADMINS = True

# Discord webhook for run summaries. Treat as a credential.
AA_DISCORD_AUDIT_WEBHOOK_URL = None

# uids skipped as bot accounts (in addition to the BotAccountUid admin
# table).
AA_DISCORD_AUDIT_BOT_UIDS = []

# Auto-discover bot accounts by Discord nickname heuristics. Reserved
# for v2; not implemented in the MVP. The startup validator raises
# ImproperlyConfigured if set to True — keep this False and use the
# explicit BotAccountUid admin table instead.
AA_DISCORD_AUDIT_AUTO_DISCOVER_BY_NICKNAME = False

# Retention. 0 disables pruning; the validator refuses 0 unless the
# acknowledged flag below is also set.
AA_DISCORD_AUDIT_RUN_RETENTION_DAYS = 180
AA_DISCORD_AUDIT_RETENTION_OPT_OUT_ACKNOWLEDGED = False

# Idempotency-key TTL. When positive, prune_audit_runs releases
# AuditRun.idempotency_key on rows older than the cutoff while the
# row itself stays for RUN_RETENTION_DAYS. 0 (default) disables the
# expiry; the key dies with the row.
AA_DISCORD_AUDIT_IDEMPOTENCY_KEY_TTL_DAYS = 0

# Per-run deadline. The Celery task's soft_time_limit follows.
AA_DISCORD_AUDIT_RUN_DEADLINE_MINUTES = 60

# Rolling 24h rate limit on accepted audit triggers per user.
# DISABLED is an opt-out gate; refuses to take effect unless
# explicitly toggled.
AA_DISCORD_AUDIT_RUN_RATE_LIMIT_PER_DAY = 5
AA_DISCORD_AUDIT_RUN_RATE_LIMIT_DISABLED = False

# Discord webhook delivery tuning.
AA_DISCORD_AUDIT_WEBHOOK_TIMEOUT = 10
AA_DISCORD_AUDIT_WEBHOOK_MAX_RETRIES = 3

# Opt-in: bulk PATCH role strip. Faster on large guilds; off by default
# while we collect operator feedback on Discord-side rate-limit shape.
AA_DISCORD_AUDIT_USE_BULK_ROLE_STRIP = False

# Periodic (Celery beat) audit. Destructive actions on the unattended
# beat require this explicit opt-in, independent of the one-shot
# first-run lock: a beat run carrying strip/kick is coerced to report
# otherwise, with a startup warning on mismatch.
AA_DISCORD_AUDIT_BEAT_ALLOW_DESTRUCTIVE = False

# Coarse global floor between beat runs, in minutes. 0 (default)
# disables it — the Celery beat schedule is the primary cadence
# control. A backstop against a misconfigured fast schedule
# (QueueOnce only dedupes concurrent ticks, not back-to-back ones).
AA_DISCORD_AUDIT_BEAT_MIN_INTERVAL_MINUTES = 0

# Pin the CLI acting user to a Django username (the "service principal"
# pattern). The management command has no HTTP request, so the actor is
# normally derived from the OS login; set this for clean, stable
# ConfigChangeLog attribution from cron and manual CLI alike. Resolved
# and permission-checked per invocation.
AA_DISCORD_AUDIT_CLI_ACTOR = None

Management commands

Command Purpose
audit_discord_roles Primary entry point. --action {report,strip,strip_kick}.
audit_discord_roles --resume <run_id> Re-walk PENDING findings of an existing run.
audit_discord_roles --abandon <run_id> Mark a stuck run as ABANDONED.
audit_discord_roles --diff <run_id> Compare current state to a historical run.
audit_discord_roles --explain <member_id> Per-member classification (read-only).
audit_discord_roles --policy-preview <json> Project a hypothetical policy.
audit_discord_roles --from-fixture <path> Replay against a JSON snapshot.
audit_acknowledge_initial Release the first-run dry-run lock from the console.
audit_benchmark Synthetic-load sizing benchmark (see docs/performance.md).
prune_audit_runs Retention pruning.
audit_abandon_stuck_runs Reconcile stuck PENDING runs leaked by a crashed worker — flips them to ABANDONED (see the runbook; a stuck RUNNING run uses audit_discord_roles --abandon).

A long-running audit_discord_roles reacts cleanly to SIGTERM and SIGINT: the run is flipped to INTERRUPTED and the apply loop drops out at the next finding boundary so partial work is durable in the audit trail. audit_discord_roles --resume <run_id> picks the run up from the remaining PENDING findings.

Periodic audit (Celery beat)

audit_orphan_members runs the same build + apply pipeline on a schedule you wire into Celery beat (it is not scheduled by default). The unattended path is gated for safety:

  • Report-only unless armed. A beat run is coerced to report regardless of policy unless AA_DISCORD_AUDIT_BEAT_ALLOW_DESTRUCTIVE is set — releasing the one-shot first-run lock for a manual CLI run does not arm the beat. A boot-time warning fires when the policy is destructive but the beat is not opted in.
  • Attributed. Every beat run writes an accepted system-actor AuditInvocation (triggered_by=BEAT), so the audit-the-auditor trail covers unattended runs too.
  • Self-healing. A run stranded by a soft-time-limit hit or a worker crash is reconciled to the resumable INTERRUPTED state on the next tick. AA_DISCORD_AUDIT_BEAT_MIN_INTERVAL_MINUTES is a coarse floor against a misconfigured fast schedule.

Celery tasks

The package registers four tasks under the aa_discord_audit.* name prefix. Only the beat tasks need a schedule; the rest are event-driven or run on demand. None are scheduled for you.

Task How it runs What it does
aa_discord_audit.audit_orphan_members Celery beat — you schedule it The unattended build + apply audit described above. Report-only unless armed; shares the discord.user_actions.<uid> lock with AA's update_groups.
aa_discord_audit.retry_pending_kicks Celery beat — you schedule it Bounded sweep that re-dispatches kicks deferred by a transient Discord failure, honouring a per-row cooldown so a hard-failing member is not hammered.
aa_discord_audit.process_pending_run Event-driven — enqueued by a web launch Picks up the PENDING run a web launch created, flips it to RUNNING, and drives the pipeline against the run's frozen confirmation flag.
aa_discord_audit.prune_audit_runs Celery beat / cron — you schedule it (also a management command) Retention: idempotency-key expiry then row deletion (see Management commands and the runbook).

Operator dashboard

Mounted under the Auth main nav as Discord Audit. Read-only views, with coloured action / state badges and sortable tables throughout. Each list carries a server-side search box that matches across every row — not just the page in view — alongside its dropdown filters:

  • Audit runs — a summary card with fleet-wide audit posture above a badged, sortable list; per-run detail splits into cards around the per-finding table.
  • Per-finding explain — classification, resolved action (with the resolved policy as JSON and an as-of marker), and which override layer (per-uid override, per-group / per-state policy, ProtectedDiscordMember, first-run lock) decided the outcome.
  • Invocations — every CLI / beat trigger including refused attempts, with the run state surfaced and the full argv expandable. Surfaces rate-limit usage.
  • Config change log — every operator edit to the operator-managed config tables, rendered as a field-level before/after diff for updates (pretty-printed JSON for create / delete) with a case-insensitive actor filter. The audit-the-auditor trail.

Launching an audit from the web UI

Operators with the aa_discord_audit.run_audit permission see a Launch Audit button on the Audit runs page. Clicking it opens a Bootstrap modal that surfaces the configured policy mode (report-only or destructive), the initial-acknowledgement state, and a Confirm Launch button. Submitting POSTs to /run-launch/, which creates a PENDING AuditRun, enqueues process_pending_run via Celery, and redirects to the run-detail page.

The web path mirrors the CLI's safety gates:

  • First-run lock. If audit_acknowledge_initial has not been run, the modal shows a refusal block (no submit button) instead of the Confirm Launch action. A POST that bypasses the modal (e.g. curl) is refused server-side, the refusal is recorded in AuditInvocation, and the operator is redirected with a flash message.
  • Destructive intent. Whether a launch may strip roles or kick members is decided server-side from the operator's permissions and the policy: was_confirmation_bypassed = user.has_perm(run_audit_destructive) and policy_has_destructive(policy). The UI has no toggle — the destructive permission alone determines intent. A run_audit-only operator who triggers a launch against a destructive policy gets a silent REPORT-only run (the same coercion the CLI applies without --yes).
  • Per-user rate limit. The same daily quota as the CLI; the modal still opens but the POST returns a flash + redirect once the quota is spent.

While a run is in a non-terminal state (PENDING, RUNNING, or INTERRUPTED), the run-detail page polls /runs/<pk>/state.json every five seconds and updates the State card in place. Polling pauses on hidden tabs and stops as soon as the run reaches a terminal state.

Observability

Optional Prometheus instrumentation behind the [metrics] extra. The module ships a cooperate-don't-depend layer: when django-prometheus is installed the audit's counters and histograms register into the default prometheus_client.REGISTRY and django-prometheus' /metrics view exports them alongside its own series. Without the extra every metric reference resolves to a no-op stub at near-zero runtime cost — the call-sites stay identical.

pip install aa-discord-audit[metrics]

Metric catalog, label vocabulary, and Grafana recipes live in docs/METRICS.md (Russian translation: docs/METRICS.ru.md).

Limitations

  • Single guild. The audit reconciles the one Discord guild AA is configured against; it does not span multiple guilds.
  • AA-managed roles only. Strip / kick act on AA-named roles and guild membership; roles AA does not manage are never touched.
  • No nickname auto-discovery. Bot accounts are recognised only via the explicit BotAccountUid table / AA_DISCORD_AUDIT_BOT_UIDSAA_DISCORD_AUDIT_AUTO_DISCOVER_BY_NICKNAME is reserved for v2 and not implemented.
  • Alpha surface. Settings names and the public API may change before 1.0.

Documentation

Development

make dev         # uv sync --all-groups + pre-commit install
make tests       # uv run nox -s tests
make lint        # uv run nox -s lint
make typecheck   # mypy + basedpyright
make coverage    # term + html + xml report
make package     # uv build

Toolchain is uv-only. Line length is 79 (Python) / 120 (Markdown).

Translations

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aa_discord_audit-0.1.0a9.tar.gz (245.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aa_discord_audit-0.1.0a9-py3-none-any.whl (279.9 kB view details)

Uploaded Python 3

File details

Details for the file aa_discord_audit-0.1.0a9.tar.gz.

File metadata

  • Download URL: aa_discord_audit-0.1.0a9.tar.gz
  • Upload date:
  • Size: 245.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for aa_discord_audit-0.1.0a9.tar.gz
Algorithm Hash digest
SHA256 1775f4754c182268a961c59df96996c3960a8975cdd9c2ba4a86311067b444ee
MD5 5a04785b6dc4c247c787b755bd9cfd4a
BLAKE2b-256 2df6044889c9827b2dddf8e64898b953e1697d23e8a67cccf02cd72db36dfd1d

See more details on using hashes here.

File details

Details for the file aa_discord_audit-0.1.0a9-py3-none-any.whl.

File metadata

  • Download URL: aa_discord_audit-0.1.0a9-py3-none-any.whl
  • Upload date:
  • Size: 279.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for aa_discord_audit-0.1.0a9-py3-none-any.whl
Algorithm Hash digest
SHA256 30c548d7d622a6d24b28198d9802dc0a9d3064ac71310954bcf7785b2ef7804c
MD5 5aea8583d3bb2bce8b633e60d5df626c
BLAKE2b-256 eaa2b82734127a078b674d4bfafca3b0b8bc2a704ca4a4a67a448c0584dd22d8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page