
Reconciliation audit for Alliance Auth's Discord integration: finds guild members holding AA-managed roles that Auth never granted and strips or kicks them under an operator-controlled policy. Standalone community Django app.
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 before1.0.
The audit is safe by default:
InitialAuditAcknowledgement (admin or shell only).report for every category — destructive
actions are opt-in.AuditRun, AuditFinding, AuditInvocation,
ConfigChangeLog) are append-only at the manager and instance
layers; bulk update() / bulk_update() are blocked.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.
allianceauth.services.modules.discord) installed and configured
(bot token + guild)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>.
Grant aa_discord_audit.run_audit to 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
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.
Re-run with the destructive action of your choice when ready.
| 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.
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