Reconciliation audit for Alliance Auth's Discord integration.
Project description
aa-discord-audit
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.
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
reportfor every category — destructive actions are opt-in. - Audit-trail rows (
AuditRun,AuditFinding,AuditInvocation,ConfigChangeLog) are append-only at the manager and instance layers; bulkupdate()/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.
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
-
Grant
aa_discord_audit.run_auditto the operator role that runs audits. -
Run a dry-run audit:
python manage.py audit_discord_roles --action report
-
Review findings under Discord Audit → Audit runs in the Auth dashboard.
-
Release the first-run lock — either create an
InitialAuditAcknowledgementrow through the admin, or runpython manage.py audit_acknowledge_initial. Both requireaa_discord_audit.run_auditandaa_discord_audit.acknowledge_initial_audit. -
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.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. |
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
reportregardless of policy unlessAA_DISCORD_AUDIT_BEAT_ALLOW_DESTRUCTIVEis 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
INTERRUPTEDstate on the next tick.AA_DISCORD_AUDIT_BEAT_MIN_INTERVAL_MINUTESis a coarse floor against a misconfigured fast schedule.
Operator dashboard
Mounted under the Auth main nav as Discord Audit. Read-only views, with coloured action / state badges and sortable, filterable DataTables throughout:
- 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.
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).
Documentation
docs/runbook.md— operator runbook: bot Discord permissions, pre-flight checklist, releasing the first-run lock, incident playbooks, diagnostic toggles.docs/performance.md—audit_benchmarkreference numbers and sizing implications.docs/METRICS.md/docs/METRICS.ru.md— Prometheus metric catalog, label vocabulary, Grafana recipes.
Development
make dev # uv sync --all-groups + pre-commit install
make test # 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).
Requirements
- Python 3.10–3.13
- Django 4.2 or 5.2
- Alliance Auth 4.x or 5.x
Translations
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aa_discord_audit-0.1.0a6.tar.gz.
File metadata
- Download URL: aa_discord_audit-0.1.0a6.tar.gz
- Upload date:
- Size: 213.1 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03ed0ee976270f804ef7f9de60c17daa35afdf4891006b088bafaa9d582e2490
|
|
| MD5 |
93c48c1a4f6e8d67be92710691cfca1b
|
|
| BLAKE2b-256 |
4484ca8bec223e49e4390786de51376169cd986d2823d12aaa0c2b53179de5b0
|
File details
Details for the file aa_discord_audit-0.1.0a6-py3-none-any.whl.
File metadata
- Download URL: aa_discord_audit-0.1.0a6-py3-none-any.whl
- Upload date:
- Size: 243.4 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0dc20ae820a0ffc381c05a3a9661b26caad5c52a1caf7c994fa749d3c47658ea
|
|
| MD5 |
d49c40183b574de7b52cee96fe875083
|
|
| BLAKE2b-256 |
31dfa17fe17dbe4a49fd0f4e2fb2c83b0906bb0094f9ced3486ead13543b7bfd
|