onbot 0.1.0.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- onbot-0.1.0.dev0/LICENSE +21 -0
- onbot-0.1.0.dev0/PKG-INFO +241 -0
- onbot-0.1.0.dev0/README.md +223 -0
- onbot-0.1.0.dev0/onbot/__init__.py +7 -0
- onbot-0.1.0.dev0/onbot/__main__.py +6 -0
- onbot-0.1.0.dev0/onbot/app.py +171 -0
- onbot-0.1.0.dev0/onbot/auth/__init__.py +19 -0
- onbot-0.1.0.dev0/onbot/auth/token_provider.py +153 -0
- onbot-0.1.0.dev0/onbot/cli.py +80 -0
- onbot-0.1.0.dev0/onbot/clients/__init__.py +5 -0
- onbot-0.1.0.dev0/onbot/clients/authentik.py +123 -0
- onbot-0.1.0.dev0/onbot/clients/base.py +243 -0
- onbot-0.1.0.dev0/onbot/clients/mas_admin.py +65 -0
- onbot-0.1.0.dev0/onbot/clients/matrix.py +457 -0
- onbot-0.1.0.dev0/onbot/clients/synapse_admin.py +159 -0
- onbot-0.1.0.dev0/onbot/clients/versions.py +61 -0
- onbot-0.1.0.dev0/onbot/config.py +653 -0
- onbot-0.1.0.dev0/onbot/events.py +51 -0
- onbot-0.1.0.dev0/onbot/healthcheck.py +134 -0
- onbot-0.1.0.dev0/onbot/identity.py +44 -0
- onbot-0.1.0.dev0/onbot/lifecycle/__init__.py +2 -0
- onbot-0.1.0.dev0/onbot/lifecycle/accounts.py +309 -0
- onbot-0.1.0.dev0/onbot/logging.py +26 -0
- onbot-0.1.0.dev0/onbot/media.py +46 -0
- onbot-0.1.0.dev0/onbot/models.py +74 -0
- onbot-0.1.0.dev0/onbot/onboarding/__init__.py +5 -0
- onbot-0.1.0.dev0/onbot/onboarding/listener.py +103 -0
- onbot-0.1.0.dev0/onbot/onboarding/welcome.py +116 -0
- onbot-0.1.0.dev0/onbot/reconciler/__init__.py +2 -0
- onbot-0.1.0.dev0/onbot/reconciler/effectors.py +78 -0
- onbot-0.1.0.dev0/onbot/reconciler/engine.py +348 -0
- onbot-0.1.0.dev0/onbot/reconciler/membership.py +54 -0
- onbot-0.1.0.dev0/onbot/reconciler/power_levels.py +96 -0
- onbot-0.1.0.dev0/onbot/reconciler/rooms.py +127 -0
- onbot-0.1.0.dev0/onbot/reconciler/state.py +77 -0
- onbot-0.1.0.dev0/onbot/utils.py +61 -0
- onbot-0.1.0.dev0/pyproject.toml +121 -0
- onbot-0.1.0.dev0/tests/README.md +17 -0
- onbot-0.1.0.dev0/tests/__init__.py +0 -0
- onbot-0.1.0.dev0/tests/contract/__init__.py +0 -0
- onbot-0.1.0.dev0/tests/contract/test_authentik_client.py +56 -0
- onbot-0.1.0.dev0/tests/contract/test_base_client.py +107 -0
- onbot-0.1.0.dev0/tests/contract/test_matrix_client.py +428 -0
- onbot-0.1.0.dev0/tests/contract/test_synapse_admin_client.py +235 -0
- onbot-0.1.0.dev0/tests/integration/__init__.py +0 -0
- onbot-0.1.0.dev0/tests/integration/conftest.py +211 -0
- onbot-0.1.0.dev0/tests/integration/stack/authentik/blueprints/onbot.yaml +53 -0
- onbot-0.1.0.dev0/tests/integration/stack/docker-compose.yml +118 -0
- onbot-0.1.0.dev0/tests/integration/stack/mas/config.yaml +131 -0
- onbot-0.1.0.dev0/tests/integration/stack/postgres/10-init-dbs.sh +12 -0
- onbot-0.1.0.dev0/tests/integration/stack/synapse/homeserver.yaml +59 -0
- onbot-0.1.0.dev0/tests/integration/stack/synapse/log.config +15 -0
- onbot-0.1.0.dev0/tests/integration/stack_api.py +325 -0
- onbot-0.1.0.dev0/tests/integration/test_lifecycle.py +52 -0
- onbot-0.1.0.dev0/tests/integration/test_media.py +22 -0
- onbot-0.1.0.dev0/tests/integration/test_onboarding.py +39 -0
- onbot-0.1.0.dev0/tests/integration/test_provisioning.py +24 -0
- onbot-0.1.0.dev0/tests/integration/test_q1_experiment.py +45 -0
- onbot-0.1.0.dev0/tests/integration/test_reconcile.py +80 -0
- onbot-0.1.0.dev0/tests/unit/__init__.py +0 -0
- onbot-0.1.0.dev0/tests/unit/test_cli.py +43 -0
- onbot-0.1.0.dev0/tests/unit/test_config.py +64 -0
- onbot-0.1.0.dev0/tests/unit/test_config_docs.py +38 -0
- onbot-0.1.0.dev0/tests/unit/test_effectors.py +31 -0
- onbot-0.1.0.dev0/tests/unit/test_engine.py +202 -0
- onbot-0.1.0.dev0/tests/unit/test_healthcheck.py +81 -0
- onbot-0.1.0.dev0/tests/unit/test_identity.py +32 -0
- onbot-0.1.0.dev0/tests/unit/test_lifecycle.py +401 -0
- onbot-0.1.0.dev0/tests/unit/test_listener.py +165 -0
- onbot-0.1.0.dev0/tests/unit/test_main_module.py +9 -0
- onbot-0.1.0.dev0/tests/unit/test_media.py +39 -0
- onbot-0.1.0.dev0/tests/unit/test_membership.py +56 -0
- onbot-0.1.0.dev0/tests/unit/test_power_levels.py +58 -0
- onbot-0.1.0.dev0/tests/unit/test_rooms.py +87 -0
- onbot-0.1.0.dev0/tests/unit/test_skeleton.py +29 -0
- onbot-0.1.0.dev0/tests/unit/test_state.py +36 -0
- onbot-0.1.0.dev0/tests/unit/test_token_provider.py +110 -0
- onbot-0.1.0.dev0/tests/unit/test_utils.py +28 -0
- onbot-0.1.0.dev0/tests/unit/test_versions.py +35 -0
- onbot-0.1.0.dev0/tests/unit/test_welcome.py +128 -0
onbot-0.1.0.dev0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Deutsche Zentrum für Diabetesforschung e.V.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: onbot
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
|
+
Summary: Keep a Matrix (Synapse) homeserver in sync with Authentik and onboard new users.
|
|
5
|
+
Keywords: matrix,synapse,authentik,mas,bot,onboarding
|
|
6
|
+
Author: DZD
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Project-URL: Homepage, https://github.com/DZD-eV-Diabetes-Research/matrix-synapse-authentik-onbaording-bot
|
|
11
|
+
Requires-Python: >=3.14
|
|
12
|
+
Requires-Dist: httpx>=0.27
|
|
13
|
+
Requires-Dist: tenacity>=9.0
|
|
14
|
+
Requires-Dist: pydantic>=2.7
|
|
15
|
+
Requires-Dist: pydantic-settings>=2.3
|
|
16
|
+
Requires-Dist: pyyaml>=6.0
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Onbot
|
|
20
|
+
|
|
21
|
+
A bot that keeps a **Matrix (Synapse)** homeserver continuously in sync with an
|
|
22
|
+
**[Authentik](https://goauthentik.io/)** identity provider and onboards every new user into the
|
|
23
|
+
right rooms with a friendly welcome. Authentik is the source of truth; Matrix mirrors it
|
|
24
|
+
(group → room, group membership → room membership, power levels), and new users get guided in with a
|
|
25
|
+
1:1 welcome DM.
|
|
26
|
+
|
|
27
|
+
Onbot targets **Matrix 2.0**: it assumes a [**Matrix Authentication Service
|
|
28
|
+
(MAS)**](https://element-hq.github.io/matrix-authentication-service/) deployment with Authentik as
|
|
29
|
+
an *upstream identity provider*, uses authenticated media, and drives the Client-Server and admin
|
|
30
|
+
APIs over a single async HTTP client (no `matrix-nio`).
|
|
31
|
+
|
|
32
|
+
See [`GOALS.md`](GOALS.md) for intent, [`BATTLE_PLAN.md`](BATTLE_PLAN.md) for the build plan, and
|
|
33
|
+
[`docs/adr/`](docs/adr/) for the architecture decisions.
|
|
34
|
+
|
|
35
|
+
> ⚠️ **Release blocker (maintainer):** the Phase 1 security items are **not done** — leaked
|
|
36
|
+
> credentials in git history still need rotating and the history scrubbing
|
|
37
|
+
> ([`BATTLE_PLAN.md`](BATTLE_PLAN.md) §5 Phase 1). **Do not publish an image or tag a release until
|
|
38
|
+
> those are complete.** The packaging below is ready; the security hand-off is the gate.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## How it works — the MAS auth topology
|
|
43
|
+
|
|
44
|
+
The auth chain is **Matrix client → MAS → Authentik** (ADR-[0006](docs/adr/0006-auth-topology-mas-authentik.md)):
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
logs in via upstream IdP
|
|
48
|
+
Matrix client ───────────────▶ MAS ◀─────────────────────── Authentik
|
|
49
|
+
▲ │ (provisions Matrix accounts (source of truth:
|
|
50
|
+
│ │ on first login) users & groups)
|
|
51
|
+
│ welcome DM, │
|
|
52
|
+
│ room membership ┌────┴─────┐
|
|
53
|
+
└──────────────────────│ Onbot │── reads users/groups ──▶ Authentik API
|
|
54
|
+
└────┬─────┘
|
|
55
|
+
└── Synapse Admin API + CS API ──▶ Synapse ◀─ MAS
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Consequences that shape how you configure Onbot:
|
|
59
|
+
|
|
60
|
+
- **Onbot does not create accounts.** MAS auto-provisions a Matrix account the first time a user
|
|
61
|
+
logs in through Authentik. Onbot's job is **projection**: turn Authentik groups into rooms, group
|
|
62
|
+
membership into room membership, and group/role attributes into power levels — plus the
|
|
63
|
+
quarantined offboarding lifecycle.
|
|
64
|
+
- **The MXID localpart contract is critical.** Onbot computes a user's MXID
|
|
65
|
+
(`@<localpart>:server_name`) from an Authentik attribute, and it **must match the localpart
|
|
66
|
+
template MAS uses** when it provisions accounts from the same Authentik claim. Set
|
|
67
|
+
[`sync_authentik_users_with_matrix_rooms.authentik_username_mapping_attribute`](docs/CONFIG_REFERENCE.md)
|
|
68
|
+
to agree with MAS — get it wrong and Onbot's computed MXIDs won't match the real accounts, so
|
|
69
|
+
nobody gets added to rooms. (Verified by the integration suite's localpart-contract test.)
|
|
70
|
+
- **Lifecycle enforcement requires MAS.** When Authentik disables a user, MAS blocks *new* logins
|
|
71
|
+
but **existing Matrix sessions keep working**, and the *Synapse* admin API cannot revoke a
|
|
72
|
+
MAS-issued session — only MAS can (ADR-[0005](docs/adr/0005-quarantine-lifecycle.md), §7 Q1, proven
|
|
73
|
+
empirically). So to actually offboard a disabled user you must configure the
|
|
74
|
+
[`mas_admin`](docs/CONFIG_REFERENCE.md) block. Without it, offboarding is a **no-op against live
|
|
75
|
+
sessions** (it silently fails to revoke).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
Configuration is a single YAML file validated by a [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)
|
|
82
|
+
model ([`onbot/config.py`](onbot/config.py)). Every setting can also be supplied (or overridden) via
|
|
83
|
+
an environment variable: prefix `ONBOT_`, nest with `__`. E.g.
|
|
84
|
+
`ONBOT_SYNAPSE_SERVER__BOT_ACCESS_TOKEN=syt_…`.
|
|
85
|
+
|
|
86
|
+
- **Full reference:** [`docs/CONFIG_REFERENCE.md`](docs/CONFIG_REFERENCE.md) — every field, its type,
|
|
87
|
+
default, description and `ONBOT_*` env-var name.
|
|
88
|
+
- **Annotated template:** [`config.example.yml`](config.example.yml) — a commented, fillable YAML
|
|
89
|
+
template. Copy it to `config.yml` and fill in the required values.
|
|
90
|
+
|
|
91
|
+
Both are **generated from the model** (with [psyplus](https://pypi.org/project/psyplus/)) and kept in
|
|
92
|
+
sync by CI — regenerate after editing `onbot/config.py`:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pdm run gen-config-docs # rewrite docs/CONFIG_REFERENCE.md + config.example.yml
|
|
96
|
+
pdm run check-config-docs # fail if they drift from the model (runs in CI)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> 🔐 **Never commit a real config.** `config*.yml` is gitignored (only `config.example.yml` is
|
|
100
|
+
> tracked) and the Docker image carries no secrets — provide config at runtime.
|
|
101
|
+
|
|
102
|
+
### Bot credentials (pick one)
|
|
103
|
+
|
|
104
|
+
Onbot authenticates to Synapse as a bot user. Under MAS, choose one of:
|
|
105
|
+
|
|
106
|
+
| Option | Field | When |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| **Compatibility token** | `synapse_server.bot_access_token` | Near-term. Issue with `mas-cli manage issue-compatibility-token`. Provide the bare token. |
|
|
109
|
+
| **OAuth2 client-credentials** | `synapse_server.oauth2` | Forward-looking. The bot is a confidential MAS client and refreshes tokens automatically. |
|
|
110
|
+
|
|
111
|
+
Provide **exactly one**. The same identity drives both the Synapse Admin API and the Client-Server
|
|
112
|
+
API.
|
|
113
|
+
|
|
114
|
+
### Minimal `config.yml`
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
synapse_server:
|
|
118
|
+
server_name: company.org # your Matrix domain (the part after the ':')
|
|
119
|
+
server_url: https://internal.matrix # how the bot reaches Synapse (internal URL is fine)
|
|
120
|
+
bot_user_id: "@welcome-bot:company.org"
|
|
121
|
+
bot_access_token: syt_REPLACE_ME # or use an `oauth2:` block instead
|
|
122
|
+
|
|
123
|
+
authentik_server:
|
|
124
|
+
url: https://authentik.company.org/
|
|
125
|
+
api_key: REPLACE_ME # Authentik API token
|
|
126
|
+
|
|
127
|
+
# Required to enforce offboarding under MAS (omit on non-MAS deployments):
|
|
128
|
+
mas_admin:
|
|
129
|
+
url: https://auth.company.org # the MAS base URL
|
|
130
|
+
client_id: REPLACE_ME # a MAS admin client (in policy.data.admin_clients)
|
|
131
|
+
client_secret: REPLACE_ME
|
|
132
|
+
|
|
133
|
+
sync_authentik_users_with_matrix_rooms:
|
|
134
|
+
authentik_username_mapping_attribute: username # MUST agree with MAS's localpart template
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
See [`config.example.yml`](config.example.yml) for everything else (room mapping rules, power
|
|
138
|
+
levels, welcome messages, the dry-run lifecycle defaults, ignore lists, …).
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Run with Docker
|
|
143
|
+
|
|
144
|
+
The published image runs as a non-root user and ships only runtime dependencies (no crypto stack —
|
|
145
|
+
the bot operates outside encrypted rooms, ADR-[0009](docs/adr/0009-e2ee-stance.md)).
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
docker run --rm \
|
|
149
|
+
-v "$PWD/config.yml:/config/config.yml:ro" \
|
|
150
|
+
ghcr.io/dzd-ev-diabetes-research/matrix-synapse-authentik-onbaording-bot:latest
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The image defaults to `ONBOT_CONFIG_FILE_PATH=/config/config.yml` and the `run` command. It has a
|
|
154
|
+
built-in `HEALTHCHECK` that calls `onbot healthcheck` (see below).
|
|
155
|
+
|
|
156
|
+
### docker-compose
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
services:
|
|
160
|
+
onbot:
|
|
161
|
+
image: ghcr.io/dzd-ev-diabetes-research/matrix-synapse-authentik-onbaording-bot:latest
|
|
162
|
+
restart: unless-stopped
|
|
163
|
+
volumes:
|
|
164
|
+
- ./config.yml:/config/config.yml:ro
|
|
165
|
+
# Or skip the file and supply everything via env (no secrets on disk):
|
|
166
|
+
# environment:
|
|
167
|
+
# ONBOT_SYNAPSE_SERVER__SERVER_NAME: company.org
|
|
168
|
+
# ONBOT_SYNAPSE_SERVER__BOT_ACCESS_TOKEN: ${ONBOT_BOT_TOKEN}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### CLI commands
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
onbot run # long-running service: reconcile loop + event-driven onboarding (default)
|
|
175
|
+
onbot reconcile-once # one idempotent reconcile pass, then exit
|
|
176
|
+
onbot generate-config # print a minimal config template (use config.example.yml for the rich one)
|
|
177
|
+
onbot healthcheck # probe Synapse/Authentik/MAS with the real credentials; exit 0 healthy, 1 not
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`onbot healthcheck` is what the container's `HEALTHCHECK` runs: it issues one authenticated request
|
|
181
|
+
to the Matrix CS API (`/whoami`), the Synapse admin API, the Authentik API, and — when `mas_admin`
|
|
182
|
+
is configured — the MAS admin API, and exits non-zero if any is unreachable or rejects the
|
|
183
|
+
credentials.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Develop
|
|
188
|
+
|
|
189
|
+
Requires **Python 3.14** and **[PDM](https://pdm-project.org/)**.
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
pdm install # create the venv and install deps (incl. dev + docs)
|
|
193
|
+
pdm run pre-commit install # enable lint + secret-scan hooks
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Run & test
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
pdm run onbot --help
|
|
200
|
+
pdm run onbot reconcile-once
|
|
201
|
+
|
|
202
|
+
pdm run pytest -m "not integration" # fast unit + contract suite
|
|
203
|
+
pdm run pytest # full suite incl. live Synapse+MAS+Authentik (needs Docker)
|
|
204
|
+
pdm run ruff check . # lint
|
|
205
|
+
pdm run ruff format --check . # formatting
|
|
206
|
+
pdm run mypy onbot # type check
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The `run_*.sh` helper scripts at the repo root wrap the same PDM commands.
|
|
210
|
+
|
|
211
|
+
### Build the image locally
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
docker build -t onbot:dev .
|
|
215
|
+
docker run --rm onbot:dev --help
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Troubleshooting
|
|
221
|
+
|
|
222
|
+
| Symptom | Likely cause |
|
|
223
|
+
|---|---|
|
|
224
|
+
| Users never get added to rooms | The MXID localpart contract is broken — `authentik_username_mapping_attribute` doesn't match MAS's localpart template, so computed MXIDs don't exist. |
|
|
225
|
+
| Disabled users keep their Matrix access | `mas_admin` is not configured (the Synapse admin API can't revoke a MAS session), or lifecycle `dry_run` is still `true` (the default). |
|
|
226
|
+
| Nothing destructive ever happens | Expected by default — the lifecycle is quarantined (`dry_run: true`); it only logs to the `onbot.lifecycle.audit` channel until you opt in. |
|
|
227
|
+
| Welcome DM send fails with a 500 | The bot device isn't registered yet — Onbot registers it on startup (`ensure_device_registered`); check startup logs. |
|
|
228
|
+
| `healthcheck` reports a dependency FAIL | Read the per-dependency log line; it distinguishes unreachable from auth-rejected. The `matrix-cs` line also flags a token/`bot_user_id` mismatch. |
|
|
229
|
+
| Sliding sync unavailable | The homeserver doesn't advertise MSC4186; Onbot falls back to the reconciler signal path automatically. |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Releasing
|
|
234
|
+
|
|
235
|
+
Versioned images are published to GHCR by the [release workflow](.github/workflows/release.yml) when
|
|
236
|
+
a `v*` tag is pushed. See [`CHANGELOG.md`](CHANGELOG.md) and the workflow's header comment for the
|
|
237
|
+
tag/version flow. **(Blocked on the Phase 1 security hand-off — see the note at the top.)**
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Onbot
|
|
2
|
+
|
|
3
|
+
A bot that keeps a **Matrix (Synapse)** homeserver continuously in sync with an
|
|
4
|
+
**[Authentik](https://goauthentik.io/)** identity provider and onboards every new user into the
|
|
5
|
+
right rooms with a friendly welcome. Authentik is the source of truth; Matrix mirrors it
|
|
6
|
+
(group → room, group membership → room membership, power levels), and new users get guided in with a
|
|
7
|
+
1:1 welcome DM.
|
|
8
|
+
|
|
9
|
+
Onbot targets **Matrix 2.0**: it assumes a [**Matrix Authentication Service
|
|
10
|
+
(MAS)**](https://element-hq.github.io/matrix-authentication-service/) deployment with Authentik as
|
|
11
|
+
an *upstream identity provider*, uses authenticated media, and drives the Client-Server and admin
|
|
12
|
+
APIs over a single async HTTP client (no `matrix-nio`).
|
|
13
|
+
|
|
14
|
+
See [`GOALS.md`](GOALS.md) for intent, [`BATTLE_PLAN.md`](BATTLE_PLAN.md) for the build plan, and
|
|
15
|
+
[`docs/adr/`](docs/adr/) for the architecture decisions.
|
|
16
|
+
|
|
17
|
+
> ⚠️ **Release blocker (maintainer):** the Phase 1 security items are **not done** — leaked
|
|
18
|
+
> credentials in git history still need rotating and the history scrubbing
|
|
19
|
+
> ([`BATTLE_PLAN.md`](BATTLE_PLAN.md) §5 Phase 1). **Do not publish an image or tag a release until
|
|
20
|
+
> those are complete.** The packaging below is ready; the security hand-off is the gate.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## How it works — the MAS auth topology
|
|
25
|
+
|
|
26
|
+
The auth chain is **Matrix client → MAS → Authentik** (ADR-[0006](docs/adr/0006-auth-topology-mas-authentik.md)):
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
logs in via upstream IdP
|
|
30
|
+
Matrix client ───────────────▶ MAS ◀─────────────────────── Authentik
|
|
31
|
+
▲ │ (provisions Matrix accounts (source of truth:
|
|
32
|
+
│ │ on first login) users & groups)
|
|
33
|
+
│ welcome DM, │
|
|
34
|
+
│ room membership ┌────┴─────┐
|
|
35
|
+
└──────────────────────│ Onbot │── reads users/groups ──▶ Authentik API
|
|
36
|
+
└────┬─────┘
|
|
37
|
+
└── Synapse Admin API + CS API ──▶ Synapse ◀─ MAS
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Consequences that shape how you configure Onbot:
|
|
41
|
+
|
|
42
|
+
- **Onbot does not create accounts.** MAS auto-provisions a Matrix account the first time a user
|
|
43
|
+
logs in through Authentik. Onbot's job is **projection**: turn Authentik groups into rooms, group
|
|
44
|
+
membership into room membership, and group/role attributes into power levels — plus the
|
|
45
|
+
quarantined offboarding lifecycle.
|
|
46
|
+
- **The MXID localpart contract is critical.** Onbot computes a user's MXID
|
|
47
|
+
(`@<localpart>:server_name`) from an Authentik attribute, and it **must match the localpart
|
|
48
|
+
template MAS uses** when it provisions accounts from the same Authentik claim. Set
|
|
49
|
+
[`sync_authentik_users_with_matrix_rooms.authentik_username_mapping_attribute`](docs/CONFIG_REFERENCE.md)
|
|
50
|
+
to agree with MAS — get it wrong and Onbot's computed MXIDs won't match the real accounts, so
|
|
51
|
+
nobody gets added to rooms. (Verified by the integration suite's localpart-contract test.)
|
|
52
|
+
- **Lifecycle enforcement requires MAS.** When Authentik disables a user, MAS blocks *new* logins
|
|
53
|
+
but **existing Matrix sessions keep working**, and the *Synapse* admin API cannot revoke a
|
|
54
|
+
MAS-issued session — only MAS can (ADR-[0005](docs/adr/0005-quarantine-lifecycle.md), §7 Q1, proven
|
|
55
|
+
empirically). So to actually offboard a disabled user you must configure the
|
|
56
|
+
[`mas_admin`](docs/CONFIG_REFERENCE.md) block. Without it, offboarding is a **no-op against live
|
|
57
|
+
sessions** (it silently fails to revoke).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
Configuration is a single YAML file validated by a [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)
|
|
64
|
+
model ([`onbot/config.py`](onbot/config.py)). Every setting can also be supplied (or overridden) via
|
|
65
|
+
an environment variable: prefix `ONBOT_`, nest with `__`. E.g.
|
|
66
|
+
`ONBOT_SYNAPSE_SERVER__BOT_ACCESS_TOKEN=syt_…`.
|
|
67
|
+
|
|
68
|
+
- **Full reference:** [`docs/CONFIG_REFERENCE.md`](docs/CONFIG_REFERENCE.md) — every field, its type,
|
|
69
|
+
default, description and `ONBOT_*` env-var name.
|
|
70
|
+
- **Annotated template:** [`config.example.yml`](config.example.yml) — a commented, fillable YAML
|
|
71
|
+
template. Copy it to `config.yml` and fill in the required values.
|
|
72
|
+
|
|
73
|
+
Both are **generated from the model** (with [psyplus](https://pypi.org/project/psyplus/)) and kept in
|
|
74
|
+
sync by CI — regenerate after editing `onbot/config.py`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pdm run gen-config-docs # rewrite docs/CONFIG_REFERENCE.md + config.example.yml
|
|
78
|
+
pdm run check-config-docs # fail if they drift from the model (runs in CI)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> 🔐 **Never commit a real config.** `config*.yml` is gitignored (only `config.example.yml` is
|
|
82
|
+
> tracked) and the Docker image carries no secrets — provide config at runtime.
|
|
83
|
+
|
|
84
|
+
### Bot credentials (pick one)
|
|
85
|
+
|
|
86
|
+
Onbot authenticates to Synapse as a bot user. Under MAS, choose one of:
|
|
87
|
+
|
|
88
|
+
| Option | Field | When |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| **Compatibility token** | `synapse_server.bot_access_token` | Near-term. Issue with `mas-cli manage issue-compatibility-token`. Provide the bare token. |
|
|
91
|
+
| **OAuth2 client-credentials** | `synapse_server.oauth2` | Forward-looking. The bot is a confidential MAS client and refreshes tokens automatically. |
|
|
92
|
+
|
|
93
|
+
Provide **exactly one**. The same identity drives both the Synapse Admin API and the Client-Server
|
|
94
|
+
API.
|
|
95
|
+
|
|
96
|
+
### Minimal `config.yml`
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
synapse_server:
|
|
100
|
+
server_name: company.org # your Matrix domain (the part after the ':')
|
|
101
|
+
server_url: https://internal.matrix # how the bot reaches Synapse (internal URL is fine)
|
|
102
|
+
bot_user_id: "@welcome-bot:company.org"
|
|
103
|
+
bot_access_token: syt_REPLACE_ME # or use an `oauth2:` block instead
|
|
104
|
+
|
|
105
|
+
authentik_server:
|
|
106
|
+
url: https://authentik.company.org/
|
|
107
|
+
api_key: REPLACE_ME # Authentik API token
|
|
108
|
+
|
|
109
|
+
# Required to enforce offboarding under MAS (omit on non-MAS deployments):
|
|
110
|
+
mas_admin:
|
|
111
|
+
url: https://auth.company.org # the MAS base URL
|
|
112
|
+
client_id: REPLACE_ME # a MAS admin client (in policy.data.admin_clients)
|
|
113
|
+
client_secret: REPLACE_ME
|
|
114
|
+
|
|
115
|
+
sync_authentik_users_with_matrix_rooms:
|
|
116
|
+
authentik_username_mapping_attribute: username # MUST agree with MAS's localpart template
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
See [`config.example.yml`](config.example.yml) for everything else (room mapping rules, power
|
|
120
|
+
levels, welcome messages, the dry-run lifecycle defaults, ignore lists, …).
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Run with Docker
|
|
125
|
+
|
|
126
|
+
The published image runs as a non-root user and ships only runtime dependencies (no crypto stack —
|
|
127
|
+
the bot operates outside encrypted rooms, ADR-[0009](docs/adr/0009-e2ee-stance.md)).
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
docker run --rm \
|
|
131
|
+
-v "$PWD/config.yml:/config/config.yml:ro" \
|
|
132
|
+
ghcr.io/dzd-ev-diabetes-research/matrix-synapse-authentik-onbaording-bot:latest
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The image defaults to `ONBOT_CONFIG_FILE_PATH=/config/config.yml` and the `run` command. It has a
|
|
136
|
+
built-in `HEALTHCHECK` that calls `onbot healthcheck` (see below).
|
|
137
|
+
|
|
138
|
+
### docker-compose
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
services:
|
|
142
|
+
onbot:
|
|
143
|
+
image: ghcr.io/dzd-ev-diabetes-research/matrix-synapse-authentik-onbaording-bot:latest
|
|
144
|
+
restart: unless-stopped
|
|
145
|
+
volumes:
|
|
146
|
+
- ./config.yml:/config/config.yml:ro
|
|
147
|
+
# Or skip the file and supply everything via env (no secrets on disk):
|
|
148
|
+
# environment:
|
|
149
|
+
# ONBOT_SYNAPSE_SERVER__SERVER_NAME: company.org
|
|
150
|
+
# ONBOT_SYNAPSE_SERVER__BOT_ACCESS_TOKEN: ${ONBOT_BOT_TOKEN}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### CLI commands
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
onbot run # long-running service: reconcile loop + event-driven onboarding (default)
|
|
157
|
+
onbot reconcile-once # one idempotent reconcile pass, then exit
|
|
158
|
+
onbot generate-config # print a minimal config template (use config.example.yml for the rich one)
|
|
159
|
+
onbot healthcheck # probe Synapse/Authentik/MAS with the real credentials; exit 0 healthy, 1 not
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`onbot healthcheck` is what the container's `HEALTHCHECK` runs: it issues one authenticated request
|
|
163
|
+
to the Matrix CS API (`/whoami`), the Synapse admin API, the Authentik API, and — when `mas_admin`
|
|
164
|
+
is configured — the MAS admin API, and exits non-zero if any is unreachable or rejects the
|
|
165
|
+
credentials.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Develop
|
|
170
|
+
|
|
171
|
+
Requires **Python 3.14** and **[PDM](https://pdm-project.org/)**.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pdm install # create the venv and install deps (incl. dev + docs)
|
|
175
|
+
pdm run pre-commit install # enable lint + secret-scan hooks
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Run & test
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
pdm run onbot --help
|
|
182
|
+
pdm run onbot reconcile-once
|
|
183
|
+
|
|
184
|
+
pdm run pytest -m "not integration" # fast unit + contract suite
|
|
185
|
+
pdm run pytest # full suite incl. live Synapse+MAS+Authentik (needs Docker)
|
|
186
|
+
pdm run ruff check . # lint
|
|
187
|
+
pdm run ruff format --check . # formatting
|
|
188
|
+
pdm run mypy onbot # type check
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The `run_*.sh` helper scripts at the repo root wrap the same PDM commands.
|
|
192
|
+
|
|
193
|
+
### Build the image locally
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
docker build -t onbot:dev .
|
|
197
|
+
docker run --rm onbot:dev --help
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Troubleshooting
|
|
203
|
+
|
|
204
|
+
| Symptom | Likely cause |
|
|
205
|
+
|---|---|
|
|
206
|
+
| Users never get added to rooms | The MXID localpart contract is broken — `authentik_username_mapping_attribute` doesn't match MAS's localpart template, so computed MXIDs don't exist. |
|
|
207
|
+
| Disabled users keep their Matrix access | `mas_admin` is not configured (the Synapse admin API can't revoke a MAS session), or lifecycle `dry_run` is still `true` (the default). |
|
|
208
|
+
| Nothing destructive ever happens | Expected by default — the lifecycle is quarantined (`dry_run: true`); it only logs to the `onbot.lifecycle.audit` channel until you opt in. |
|
|
209
|
+
| Welcome DM send fails with a 500 | The bot device isn't registered yet — Onbot registers it on startup (`ensure_device_registered`); check startup logs. |
|
|
210
|
+
| `healthcheck` reports a dependency FAIL | Read the per-dependency log line; it distinguishes unreachable from auth-rejected. The `matrix-cs` line also flags a token/`bot_user_id` mismatch. |
|
|
211
|
+
| Sliding sync unavailable | The homeserver doesn't advertise MSC4186; Onbot falls back to the reconciler signal path automatically. |
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Releasing
|
|
216
|
+
|
|
217
|
+
Versioned images are published to GHCR by the [release workflow](.github/workflows/release.yml) when
|
|
218
|
+
a `v*` tag is pushed. See [`CHANGELOG.md`](CHANGELOG.md) and the workflow's header comment for the
|
|
219
|
+
tag/version flow. **(Blocked on the Phase 1 security hand-off — see the note at the top.)**
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Onbot — keep a Matrix homeserver in sync with Authentik and onboard new users.
|
|
2
|
+
|
|
3
|
+
Clean-slate rebuild targeting modern Matrix (MAS / next-gen auth, authenticated media,
|
|
4
|
+
sliding sync). See ``BATTLE_PLAN.md`` for the architecture and ``GOALS.md`` for intent.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0.dev0"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Composition root: build clients, wire the reconciler + onboarding, manage lifecycle.
|
|
2
|
+
|
|
3
|
+
Keeps construction in one place (AD-4) so the CLI stays thin. The reconciler converges
|
|
4
|
+
Authentik→Matrix state; onboarding reacts (event-driven) to the reconciler's "user provisioned"
|
|
5
|
+
signal and to Matrix join events. They share the async API clients and an in-process event bus.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from collections.abc import AsyncIterator
|
|
12
|
+
from contextlib import asynccontextmanager
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from onbot.auth.token_provider import (
|
|
16
|
+
OAuth2ClientCredentialsTokenProvider,
|
|
17
|
+
StaticTokenProvider,
|
|
18
|
+
TokenProvider,
|
|
19
|
+
)
|
|
20
|
+
from onbot.clients.authentik import ApiClientAuthentik
|
|
21
|
+
from onbot.clients.mas_admin import ApiClientMasAdmin
|
|
22
|
+
from onbot.clients.matrix import ApiClientMatrix, CSApiEffectors
|
|
23
|
+
from onbot.clients.synapse_admin import ApiClientSynapseAdmin
|
|
24
|
+
from onbot.config import OnbotConfig, SynapseServer
|
|
25
|
+
from onbot.events import EventBus
|
|
26
|
+
from onbot.lifecycle.accounts import (
|
|
27
|
+
AccountLifecycleManager,
|
|
28
|
+
AdminApiLifecycleEffectors,
|
|
29
|
+
LifecycleEffectors,
|
|
30
|
+
MasLifecycleEffectors,
|
|
31
|
+
MatrixAccountDataLedgerStore,
|
|
32
|
+
)
|
|
33
|
+
from onbot.logging import get_logger
|
|
34
|
+
from onbot.media import MediaUploader
|
|
35
|
+
from onbot.onboarding.listener import OnboardingListener
|
|
36
|
+
from onbot.onboarding.welcome import WelcomeService
|
|
37
|
+
from onbot.reconciler.engine import ReconcilerEngine
|
|
38
|
+
|
|
39
|
+
log = get_logger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_matrix_token_provider(synapse: SynapseServer) -> TokenProvider:
|
|
43
|
+
"""Pick the bot's auth strategy (AD-6): OAuth2 client-credentials if configured, else a static
|
|
44
|
+
compatibility/legacy token. Raises if neither is provided."""
|
|
45
|
+
if synapse.oauth2 is not None:
|
|
46
|
+
return OAuth2ClientCredentialsTokenProvider(
|
|
47
|
+
token_endpoint=synapse.oauth2.token_endpoint,
|
|
48
|
+
client_id=synapse.oauth2.client_id,
|
|
49
|
+
client_secret=synapse.oauth2.client_secret,
|
|
50
|
+
scope=synapse.oauth2.scope,
|
|
51
|
+
)
|
|
52
|
+
if synapse.bot_access_token:
|
|
53
|
+
return StaticTokenProvider(synapse.bot_access_token)
|
|
54
|
+
raise ValueError("synapse_server needs either bot_access_token or an oauth2 block")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _apply_bot_avatar(matrix: ApiClientMatrix, config: OnbotConfig) -> None:
|
|
58
|
+
"""Set the bot's own avatar from the configured URL on startup (G6.8), best-effort."""
|
|
59
|
+
url = config.synapse_server.bot_avatar_url
|
|
60
|
+
if not url:
|
|
61
|
+
return
|
|
62
|
+
uploader = MediaUploader(matrix)
|
|
63
|
+
try:
|
|
64
|
+
mxc = await uploader.upload_from_url(url)
|
|
65
|
+
await matrix.set_user_avatar(config.synapse_server.bot_user_id, mxc)
|
|
66
|
+
log.info("set bot avatar from %s", url)
|
|
67
|
+
except Exception:
|
|
68
|
+
log.exception("failed to set bot avatar from %s", url)
|
|
69
|
+
finally:
|
|
70
|
+
await uploader.aclose()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(slots=True)
|
|
74
|
+
class App:
|
|
75
|
+
"""Wired application: the reconciler engine and the onboarding listener over a shared bus."""
|
|
76
|
+
|
|
77
|
+
engine: ReconcilerEngine
|
|
78
|
+
listener: OnboardingListener
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@asynccontextmanager
|
|
82
|
+
async def build_app(config: OnbotConfig) -> AsyncIterator[App]:
|
|
83
|
+
"""Construct the reconciler + onboarding with their clients, closing them on exit."""
|
|
84
|
+
authentik = ApiClientAuthentik(
|
|
85
|
+
url=config.authentik_server.url,
|
|
86
|
+
api_key=config.authentik_server.api_key,
|
|
87
|
+
)
|
|
88
|
+
# One MAS-aware token provider shared by the admin + CS clients (same bot identity, AD-6).
|
|
89
|
+
token_provider = build_matrix_token_provider(config.synapse_server)
|
|
90
|
+
admin = ApiClientSynapseAdmin(
|
|
91
|
+
server_url=config.synapse_server.server_url,
|
|
92
|
+
token_provider=token_provider,
|
|
93
|
+
admin_api_path=config.synapse_server.admin_api_path,
|
|
94
|
+
)
|
|
95
|
+
matrix = ApiClientMatrix(
|
|
96
|
+
server_url=config.synapse_server.server_url,
|
|
97
|
+
token_provider=token_provider,
|
|
98
|
+
server_name=config.synapse_server.server_name,
|
|
99
|
+
)
|
|
100
|
+
# Negotiate CS-API capabilities up front (sliding sync / authenticated media); best-effort so a
|
|
101
|
+
# transient failure does not block startup — the listener re-checks and falls back if needed.
|
|
102
|
+
try:
|
|
103
|
+
await matrix.negotiate_versions()
|
|
104
|
+
except Exception:
|
|
105
|
+
log.exception("CS-API version negotiation failed; continuing with defaults")
|
|
106
|
+
# Register the bot's device so welcome DM sends work under MAS (compat-token devices are
|
|
107
|
+
# otherwise absent from Synapse's devices table; ADR-0006/0009).
|
|
108
|
+
await matrix.ensure_device_registered()
|
|
109
|
+
await _apply_bot_avatar(matrix, config)
|
|
110
|
+
events = EventBus()
|
|
111
|
+
# Lifecycle enforcement: under MAS only the MAS admin API can revoke a live session (§7 Q1), so
|
|
112
|
+
# prefer it when configured; otherwise fall back to the Synapse-admin effectors.
|
|
113
|
+
mas_admin: ApiClientMasAdmin | None = None
|
|
114
|
+
lifecycle_effectors: LifecycleEffectors
|
|
115
|
+
if config.mas_admin is not None:
|
|
116
|
+
mas_admin = ApiClientMasAdmin(
|
|
117
|
+
mas_url=config.mas_admin.url,
|
|
118
|
+
token_provider=OAuth2ClientCredentialsTokenProvider(
|
|
119
|
+
token_endpoint=f"{config.mas_admin.url.rstrip('/')}/oauth2/token",
|
|
120
|
+
client_id=config.mas_admin.client_id,
|
|
121
|
+
client_secret=config.mas_admin.client_secret,
|
|
122
|
+
scope="urn:mas:admin",
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
lifecycle_effectors = MasLifecycleEffectors(mas_admin, synapse_admin=admin)
|
|
126
|
+
else:
|
|
127
|
+
lifecycle_effectors = AdminApiLifecycleEffectors(admin)
|
|
128
|
+
lifecycle = AccountLifecycleManager(
|
|
129
|
+
config,
|
|
130
|
+
store=MatrixAccountDataLedgerStore(
|
|
131
|
+
matrix, config.synapse_server.bot_user_id, config.synapse_server.server_name
|
|
132
|
+
),
|
|
133
|
+
effectors=lifecycle_effectors,
|
|
134
|
+
)
|
|
135
|
+
engine = ReconcilerEngine(
|
|
136
|
+
config, authentik, admin, effectors=CSApiEffectors(matrix), events=events, lifecycle=lifecycle
|
|
137
|
+
)
|
|
138
|
+
welcome = WelcomeService(matrix, config)
|
|
139
|
+
listener = OnboardingListener(matrix, welcome, config, events)
|
|
140
|
+
listener.start() # subscribe onboarding to the reconciler's user-provisioned signal (AD-4)
|
|
141
|
+
try:
|
|
142
|
+
yield App(engine=engine, listener=listener)
|
|
143
|
+
finally:
|
|
144
|
+
await authentik.aclose()
|
|
145
|
+
await admin.aclose()
|
|
146
|
+
await matrix.aclose()
|
|
147
|
+
if mas_admin is not None:
|
|
148
|
+
await mas_admin.aclose()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def run_service(config: OnbotConfig) -> None:
|
|
152
|
+
"""Run the reconcile loop and the onboarding listener concurrently until stopped."""
|
|
153
|
+
async with build_app(config) as app:
|
|
154
|
+
# The engine owns the signal handlers; when it stops, stop the listener too.
|
|
155
|
+
async def _reconcile() -> None:
|
|
156
|
+
try:
|
|
157
|
+
await app.engine.run()
|
|
158
|
+
finally:
|
|
159
|
+
app.listener.request_stop()
|
|
160
|
+
|
|
161
|
+
await asyncio.gather(_reconcile(), app.listener.run())
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def run_reconcile_once(config: OnbotConfig) -> None:
|
|
165
|
+
"""Run a single reconcile pass and exit (``onbot reconcile-once``).
|
|
166
|
+
|
|
167
|
+
Onboarding still fires for users discovered this pass — the listener is subscribed to the bus —
|
|
168
|
+
but the long-running sync stream is not started.
|
|
169
|
+
"""
|
|
170
|
+
async with build_app(config) as app:
|
|
171
|
+
await app.engine.reconcile_once()
|