buntu 0.1.0__py3-none-any.whl
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.
- buntu-0.1.0.dist-info/METADATA +141 -0
- buntu-0.1.0.dist-info/RECORD +90 -0
- buntu-0.1.0.dist-info/WHEEL +5 -0
- buntu-0.1.0.dist-info/licenses/LICENSE +21 -0
- buntu-0.1.0.dist-info/top_level.txt +1 -0
- idp/__init__.py +0 -0
- idp/app.py +114 -0
- idp/cli.py +340 -0
- idp/config.py +121 -0
- idp/db/__init__.py +76 -0
- idp/extensions.py +6 -0
- idp/models/__init__.py +54 -0
- idp/models/account.py +145 -0
- idp/models/clients.py +155 -0
- idp/models/grants.py +157 -0
- idp/models/platform.py +253 -0
- idp/models/users.py +87 -0
- idp/oauth/__init__.py +0 -0
- idp/oauth/claims.py +33 -0
- idp/oauth/grants.py +320 -0
- idp/oauth/jwt.py +101 -0
- idp/oauth/server.py +369 -0
- idp/plugins/__init__.py +270 -0
- idp/plugins/sync.py +152 -0
- idp/security/__init__.py +0 -0
- idp/security/crypto.py +30 -0
- idp/security/csrf.py +34 -0
- idp/security/data/breached.txt.gz +0 -0
- idp/security/keys.py +67 -0
- idp/security/passwords.py +48 -0
- idp/security/tokens.py +40 -0
- idp/security/totp.py +56 -0
- idp/services/__init__.py +0 -0
- idp/services/accounts.py +452 -0
- idp/services/activity.py +73 -0
- idp/services/audit.py +22 -0
- idp/services/backchannel.py +142 -0
- idp/services/closing.py +147 -0
- idp/services/connected.py +177 -0
- idp/services/consent.py +25 -0
- idp/services/credentials.py +58 -0
- idp/services/devices.py +30 -0
- idp/services/mail.py +86 -0
- idp/services/mfa.py +155 -0
- idp/services/own_apps.py +24 -0
- idp/services/passkeys.py +252 -0
- idp/services/platform.py +1024 -0
- idp/services/profile.py +190 -0
- idp/services/retention.py +90 -0
- idp/services/security_prompt.py +18 -0
- idp/services/session_check.py +91 -0
- idp/services/sessions.py +217 -0
- idp/services/setup.py +164 -0
- idp/static/idp/assets/account-D-MHfC1z.js +1 -0
- idp/static/idp/assets/account-DN_Addf0.js +1 -0
- idp/static/idp/assets/app-CzfYFveQ.js +147 -0
- idp/static/idp/assets/di-D0iZ5SU4.js +1 -0
- idp/static/idp/assets/di-dx_XE3Av.js +1 -0
- idp/static/idp/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- idp/static/idp/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- idp/static/idp/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- idp/static/idp/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- idp/static/idp/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- idp/static/idp/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- idp/static/idp/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- idp/static/idp/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- idp/static/idp/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- idp/static/idp/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- idp/static/idp/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- idp/static/idp/assets/i18n-BZyjOlbu.js +1 -0
- idp/static/idp/assets/i18n-Dog5o8z6.js +1 -0
- idp/static/idp/assets/roboto-latin-variable-ccAYIvAh.woff2 +0 -0
- idp/static/idp/assets/schemas-Bl_iabpv.js +65 -0
- idp/static/idp/assets/secure-account-Dl1pS_Wo.js +1 -0
- idp/static/idp/assets/service-BhG4-X2O.js +1 -0
- idp/static/idp/assets/service-vCuiuvPs.js +1 -0
- idp/static/idp/assets/theme-toggle-C07m3YIv.js +1 -0
- idp/static/idp/assets/two-step-section-DMQmuG2q.js +2 -0
- idp/static/idp/assets/webauthn-BQas4tAd.js +1 -0
- idp/static/idp/idp.css +2 -0
- idp/static/idp/idp.js +2 -0
- idp/templates/shell.html +15 -0
- idp/web/__init__.py +0 -0
- idp/web/admin.py +901 -0
- idp/web/api.py +717 -0
- idp/web/developer.py +628 -0
- idp/web/oidc.py +394 -0
- idp/web/openapi.py +1158 -0
- idp/web/pages.py +238 -0
- idp/web/validation.py +46 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: buntu
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OAuth 2.1 and OpenID Connect provider
|
|
5
|
+
Author: Ibrahima Touré
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Source, https://github.com/buntubi/buntu
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: flask>=3.1
|
|
12
|
+
Requires-Dist: authlib<1.9,>=1.8
|
|
13
|
+
Requires-Dist: sqlalchemy>=2.0.36
|
|
14
|
+
Requires-Dist: alembic>=1.14
|
|
15
|
+
Requires-Dist: psycopg[binary]>=3.2
|
|
16
|
+
Requires-Dist: pydantic-settings>=2.6
|
|
17
|
+
Requires-Dist: flask-limiter>=3.8
|
|
18
|
+
Requires-Dist: requests>=2.32
|
|
19
|
+
Requires-Dist: segno>=1.6
|
|
20
|
+
Requires-Dist: email-validator>=2.2
|
|
21
|
+
Requires-Dist: webauthn<4,>=3.0
|
|
22
|
+
Requires-Dist: cryptography>=44
|
|
23
|
+
Requires-Dist: joserfc<2,>=1.7
|
|
24
|
+
Requires-Dist: gunicorn>=23
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.13; extra == "dev"
|
|
29
|
+
Requires-Dist: openapi-spec-validator>=0.7; extra == "dev"
|
|
30
|
+
Requires-Dist: jsonschema>=4.23; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# The provider
|
|
34
|
+
|
|
35
|
+
The OAuth 2.1 and OpenID Connect server, in Flask. It owns every account, every session and
|
|
36
|
+
every token on the platform, and every rule about who may do what. The console, the portal and
|
|
37
|
+
the sign-in pages hold no rules of their own: they call this server and show what it answers.
|
|
38
|
+
|
|
39
|
+
Setup, deployment and the platform's concepts (accounts, organizations, apps, the catalogue) are
|
|
40
|
+
in the [repository README](../README.md). Plugins are in [docs/plugins.md](../docs/plugins.md).
|
|
41
|
+
|
|
42
|
+
## What it serves
|
|
43
|
+
|
|
44
|
+
| Prefix | Who calls it | What |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `/.well-known/openid-configuration`, `/.well-known/jwks.json` | Any client | Discovery and the signing keys |
|
|
47
|
+
| `/.well-known/brand.json` | The console and the portal | The platform's name, colour and links |
|
|
48
|
+
| `/oauth/*` | OAuth clients | `authorize`, `token`, `revoke`, `introspect`, `device_authorization`, `userinfo`, `logout`, `check-session` |
|
|
49
|
+
| `/signin`, `/signup`, `/mfa`, `/consent`, `/accounts`, `/account/*`, ... | People, in a browser | The pages built from `frontend/`, rendered into `templates/shell.html` |
|
|
50
|
+
| `/api/*` | Those pages | Sign-in steps, the account area, passkeys. Same-site, with a CSRF token |
|
|
51
|
+
| `/developer/api/*` | The portal | Organizations, members, apps, secrets, test users, API keys. Bearer token with the `developer` scope |
|
|
52
|
+
| `/admin/api/*` | The console | Users, organizations, reviews, the catalogue, the platform's apps. Bearer token with the `admin` scope and a two-step sign-in |
|
|
53
|
+
| `/api/plugins/<id>/*` | Plugin pages in the console and portal | Each plugin's own API |
|
|
54
|
+
| `/openapi.json` | Developers, Postman | The OAuth endpoints, the developer API and the admin API |
|
|
55
|
+
|
|
56
|
+
Grants: authorization code with PKCE, refresh token (rotated, with reuse detection), client
|
|
57
|
+
credentials, and the device code. Tokens are JWTs signed with the newest key in
|
|
58
|
+
`IDP_SIGNING_KEYS_DIR`; every key in the directory still verifies.
|
|
59
|
+
|
|
60
|
+
Sessions end everywhere they reached: a sign-out, a device signed out from the account area,
|
|
61
|
+
or a deleted account sends an OpenID back-channel logout token to every app with a
|
|
62
|
+
`backchannel_logout_uri` that took part in that session.
|
|
63
|
+
|
|
64
|
+
## Layout
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
src/idp/
|
|
68
|
+
app.py create_app: settings, extensions, blueprints, plugins
|
|
69
|
+
config.py every setting, read from IDP_* variables and .env
|
|
70
|
+
cli.py the flask commands
|
|
71
|
+
models/ SQLAlchemy models
|
|
72
|
+
db/ the session and utcnow
|
|
73
|
+
oauth/ Authlib server, grants, claims, JWT helpers
|
|
74
|
+
security/ passwords, TOTP, CSRF, signing keys, encryption at rest
|
|
75
|
+
services/ the rules: accounts, sessions, MFA, passkeys, platform, profile, ...
|
|
76
|
+
web/ the blueprints: pages, api, developer, admin, oidc, openapi
|
|
77
|
+
plugins/ the plugin interface and the catalogue sync
|
|
78
|
+
templates/ the page shell
|
|
79
|
+
static/idp/ the built sign-in pages (from frontend/, not edited here)
|
|
80
|
+
migrations/ Alembic revisions
|
|
81
|
+
tests/ pytest, against a real PostgreSQL database
|
|
82
|
+
platform.example.toml the platform's apps, copied to platform.toml per deployment
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The rule of thumb when changing something: a view in `web/` parses the request and shapes the
|
|
86
|
+
answer; the decision lives in `services/`. A rule that two endpoints share is one function in a
|
|
87
|
+
service, never two copies in two views.
|
|
88
|
+
|
|
89
|
+
## Commands
|
|
90
|
+
|
|
91
|
+
Run from `idp/` as `../.venv/bin/flask --app idp.app:create_app <command>`.
|
|
92
|
+
|
|
93
|
+
| Command | Does |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `keys generate` | Adds a signing key; the newest signs from then on |
|
|
96
|
+
| `admin create`, `admin check` | Creates the superadmin; checks one exists |
|
|
97
|
+
| `users admins`, `users promote`, `users demote` | Lists admins; gives or takes the admin role |
|
|
98
|
+
| `users delete <pattern> --yes` | Deletes accounts by email (a `*` pattern works), as deleting from the account page does: apps are signed out, plugins told, the audit trail kept without who it was |
|
|
99
|
+
| `prune` | Deletes expired codes and tokens, sign-ins ended more than `IDP_SESSION_RETENTION_DAYS` ago and audit rows older than `IDP_AUDIT_RETENTION_DAYS`; schedule it daily |
|
|
100
|
+
| `platform setup [--file] [--rotate-secrets]` | The platform organization, the apps `platform.toml` lists, and the plugins' catalogue |
|
|
101
|
+
| `clients create`, `clients homepage <client_id> [url]` | A client by hand; set or clear the address it lives at |
|
|
102
|
+
| `plugins list`, `plugins sync` | The plugins running; bring the catalogue in line with them |
|
|
103
|
+
|
|
104
|
+
Migrations: `../.venv/bin/alembic upgrade head`, with `IDP_DATABASE_URL` set. A new revision
|
|
105
|
+
goes in `migrations/versions/` and names its `down_revision`; there is one head.
|
|
106
|
+
|
|
107
|
+
## Settings
|
|
108
|
+
|
|
109
|
+
All of them are in `src/idp/config.py`, each with a comment, and read from the environment with
|
|
110
|
+
the `IDP_` prefix or from `idp/.env` (see `.env.example`). The ones every deployment sets:
|
|
111
|
+
|
|
112
|
+
| Setting | Meaning |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `IDP_ISSUER` | The public origin, exactly as clients see it, no trailing slash |
|
|
115
|
+
| `IDP_DATABASE_URL` | PostgreSQL |
|
|
116
|
+
| `IDP_SECRET_KEY` | Signs the session cookie and CSRF tokens; 32 characters or more |
|
|
117
|
+
| `IDP_SIGNING_KEYS_DIR` | The token signing keys |
|
|
118
|
+
| `IDP_EMAIL_TEST_MODE`, `IDP_RESEND_API_KEY`, `IDP_EMAIL_FROM` | Email: logged in test mode (development only), sent through Resend otherwise |
|
|
119
|
+
| `IDP_TRUSTED_PROXIES` | How many proxies in front set `X-Forwarded-For` and `X-Forwarded-Proto`; 0 when none |
|
|
120
|
+
| `IDP_RATE_LIMIT_STORAGE` | Where rate limits are counted: `redis://...` shared by every process, or `memory://` for one |
|
|
121
|
+
| `IDP_BRAND_*` | The platform's name, colour, logo and links |
|
|
122
|
+
| `IDP_CORS_ORIGINS` | Browser origins allowed on the token, revocation and userinfo endpoints |
|
|
123
|
+
| `IDP_PLUGINS`, `IDP_PLUGINS_SYNC_ON_START` | The plugins, as a JSON list (`["acme"]`); whether to sync the catalogue at start |
|
|
124
|
+
|
|
125
|
+
With an issuer that is not on this machine, the provider refuses to start when the issuer is
|
|
126
|
+
not https, email test mode is on, cookies are not Secure, or `IDP_RATE_LIMIT_STORAGE` is not
|
|
127
|
+
set. It then also sends `Strict-Transport-Security`.
|
|
128
|
+
|
|
129
|
+
Lifetimes (`IDP_ACCESS_TOKEN_TTL`, `IDP_REFRESH_TOKEN_TTL`, `IDP_SESSION_TTL`, ...) are in
|
|
130
|
+
seconds. What `prune` keeps is in days: `IDP_SESSION_RETENTION_DAYS` (30) for ended sign-ins,
|
|
131
|
+
`IDP_AUDIT_RETENTION_DAYS` (365) for the audit trail.
|
|
132
|
+
|
|
133
|
+
## Checks
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
../.venv/bin/python -m pytest # needs the idp_test database on localhost
|
|
137
|
+
../.venv/bin/ruff check src tests
|
|
138
|
+
../.venv/bin/mypy src
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Every change to a rule comes with a test in `tests/` that fails without it.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
buntu-0.1.0.dist-info/licenses/LICENSE,sha256=0i_cY3H9AZOVIDYj7922S_jN83vhQNdPg9sm2HqB4R8,1072
|
|
2
|
+
idp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
idp/app.py,sha256=ztmtbSu1mM2fnfEnB-3oUtbDcjGL_5gld7xX1etNLjo,4989
|
|
4
|
+
idp/cli.py,sha256=VsLT8ANUcaeYYAAE4MrS_pL7m2pSFpqTiDF18MRHFKQ,14995
|
|
5
|
+
idp/config.py,sha256=jYKMUOG7Tz4bbZRcBjgq37QHz2pA7fEcJH9LyKWql9A,5010
|
|
6
|
+
idp/extensions.py,sha256=XO3ARXfgVmdByvuZytY1dmMsNs5yaQweyAxjj3kztyE,211
|
|
7
|
+
idp/db/__init__.py,sha256=28UETZvOaB2CSlUQZz38ZdqZBgVHlK1vNHhuw5scBcM,2232
|
|
8
|
+
idp/models/__init__.py,sha256=B4k0NSAti0bfWyUUHsDKZ0XThE2CjNvvXdznjNga4Jc,1110
|
|
9
|
+
idp/models/account.py,sha256=Y9uhryZ77R_EBEjOguse1FeBbabYNCFpe68ttb3s6tQ,6417
|
|
10
|
+
idp/models/clients.py,sha256=cjAC9iZbYLea_Y1shC0z0UhoPq0_UC_WBI3RrI7MIbo,7750
|
|
11
|
+
idp/models/grants.py,sha256=TIbNw6LrjSAcIhGeiUa3VyHCO25JApLuqcvM6DdHnB0,6912
|
|
12
|
+
idp/models/platform.py,sha256=Ocwwnj9uOuef4W2akmBr6m1nJMMH5OKAX-5XDfUgK-c,11927
|
|
13
|
+
idp/models/users.py,sha256=z3s5P5qYDbAaQ0RuhFu8Bc5uy_qgAjIaMgf0ppbr1u0,4070
|
|
14
|
+
idp/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
idp/oauth/claims.py,sha256=8Uv2i44ivuZwQ_Ts9Ga09GU3CpU_NjrkpYsH2r0djKU,1290
|
|
16
|
+
idp/oauth/grants.py,sha256=97fsLMALs0V0pJRnpLVPOmIYvKSTOunq_3R9pL0NHxE,12494
|
|
17
|
+
idp/oauth/jwt.py,sha256=7gNbEeVfI5j5Lhzjv-02G-LTtL4BPnbcb9dTVHnpLlw,2988
|
|
18
|
+
idp/oauth/server.py,sha256=Z4teDppC24gejuDmt9I0BQyFPW5iByJxVB3JmsxZLSQ,14716
|
|
19
|
+
idp/plugins/__init__.py,sha256=1Xr0cILaW0vPSZHTSCMaYDVt1SxV4gk3oHPoPF4ulKM,8771
|
|
20
|
+
idp/plugins/sync.py,sha256=oeF7HHh1zQBP6b5WDvVIuOptzOj-a_lk0uYeHN11kq4,6572
|
|
21
|
+
idp/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
idp/security/crypto.py,sha256=7vzVGMni4lfFRrgz938aqbmXmzSxr1g2neytT-1eGsE,867
|
|
23
|
+
idp/security/csrf.py,sha256=BZ60FXYstIX6SXeQE7dFzZ_TLDquvfOPESQvoLh20VI,966
|
|
24
|
+
idp/security/keys.py,sha256=piOIBTtn0lCewBdhBveEr0qRXe_YMmGVS6Fi6JLRNKA,2330
|
|
25
|
+
idp/security/passwords.py,sha256=6lppGt-hqv4h-vES9LaFLj9lbyTyPFhHiNTTP9TfdYI,1517
|
|
26
|
+
idp/security/tokens.py,sha256=vL9YN_imgCexnyg8NhnH17ivc_ZVVNWDdHI0WDPP1QQ,1322
|
|
27
|
+
idp/security/totp.py,sha256=73XDq5kOrg4L4PMNW11YRDeRMd03ekmxabUDs-g8vxU,1940
|
|
28
|
+
idp/security/data/breached.txt.gz,sha256=ACk-h0bGJld_dE3pNxI2a0EDBGt4Hdj0hAMVmF_ekss,681212
|
|
29
|
+
idp/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
idp/services/accounts.py,sha256=BZBcoJx0GuwzchRrOyZMRQQPQDzRI10JfGTBginHtsg,17510
|
|
31
|
+
idp/services/activity.py,sha256=Wfet1b0o51N50cOCH0JAW2x7b-VMJOHqxW8XGIv2nHg,2605
|
|
32
|
+
idp/services/audit.py,sha256=xf_bVDvwxfN9vSSLZhrTJSVM_jaG5EI5gl21wkm8yrQ,805
|
|
33
|
+
idp/services/backchannel.py,sha256=oqX9MdIMQvuGuDNkXJOQDq6hFCWCDA1qb1EvheLv1Ec,5159
|
|
34
|
+
idp/services/closing.py,sha256=2v8SOWR-7J5RVy5QQWcHQK1HMg8eNbaQ6WttQYv-ZWQ,5573
|
|
35
|
+
idp/services/connected.py,sha256=rnn47m3Y1s2heNcTEr4celVkLIN_F-HSeGXKBHdFWTk,7387
|
|
36
|
+
idp/services/consent.py,sha256=OrtFtkqIhSOMdJluSBOZX0MGlHU4il5evTh2S4-mJXM,1038
|
|
37
|
+
idp/services/credentials.py,sha256=facYeHwlgOrMyxnE0o6o8X-XnvatzwEvS04HnFS-hdg,2033
|
|
38
|
+
idp/services/devices.py,sha256=KMNeeDJC91wIel5H_3qqUXJNK5RP_l2ow2FsHsAG6jk,1005
|
|
39
|
+
idp/services/mail.py,sha256=DNk0MOcHlu4EN7w_Y-Dr_sRriVTDGB4AaTl4W0siEd0,2902
|
|
40
|
+
idp/services/mfa.py,sha256=4lxgRoYePKdmQmIq6hRhNgLYwQaMRnSyYo6V9QT_bjk,5656
|
|
41
|
+
idp/services/own_apps.py,sha256=8Qdardppnyh7RvyMI-dn77xhXkY7wI8ota1aawxtIyk,787
|
|
42
|
+
idp/services/passkeys.py,sha256=HN5OujIgow8e-jt2NfwmOWyCjS9Nuyh3G1gSsg5TT9o,9333
|
|
43
|
+
idp/services/platform.py,sha256=IrgDowFrk-gO02w05eWDgA5QM0y6bCt3Mw64im-QO7U,42280
|
|
44
|
+
idp/services/profile.py,sha256=uwyP1HBZM2g4GDocowN-1NtQd9r-wiWhXPQ09sJ8kCA,7214
|
|
45
|
+
idp/services/retention.py,sha256=TeETx5o1EO6rsJxeLYKJlE9HeWZcEGk4FTYQ64M0k3s,3523
|
|
46
|
+
idp/services/security_prompt.py,sha256=ozRuF6s-RYbaISO1jgfiAx2Da6Uc_dAzk8EjbiqhZQk,660
|
|
47
|
+
idp/services/session_check.py,sha256=-Nubu-VjwKGfLAJfu-3TmgNH3FFA_sFHBwvIYTCaQzQ,3515
|
|
48
|
+
idp/services/sessions.py,sha256=uQHs9PrWNCjjufZMlft2z4TR8VJnD7YtR6UZfkm6-is,7541
|
|
49
|
+
idp/services/setup.py,sha256=14ThMoLzuGY00EpqrBmEVHrx9R3GYlvKp505v8_UPFU,6535
|
|
50
|
+
idp/static/idp/idp.css,sha256=ZQT1KQWJWUWUcAod-47KVN1h_n8wGm0kUMc_kRK2g84,154483
|
|
51
|
+
idp/static/idp/idp.js,sha256=AkOWkjlv6JWaR_dgsK8EyoujaSpXWikZekCWMPbohvc,62598
|
|
52
|
+
idp/static/idp/assets/account-D-MHfC1z.js,sha256=ovkoMR8zC8ixljQAwng45VAyt3en_TAdZpcZoLFnL50,66
|
|
53
|
+
idp/static/idp/assets/account-DN_Addf0.js,sha256=Q6qyIt_Dvxai63lQdP6FOSl7xe51QmCZaZGtMYTPSM4,48
|
|
54
|
+
idp/static/idp/assets/app-CzfYFveQ.js,sha256=XJTSpi8sU6e05Dr59sDrE8BJSTv8Bgv0FtTxqwckCJ8,194891
|
|
55
|
+
idp/static/idp/assets/di-D0iZ5SU4.js,sha256=8nDpOVoKz3qEbvh3vn1bpCdiYqgEMA6zilcRne2POZ4,75973
|
|
56
|
+
idp/static/idp/assets/di-dx_XE3Av.js,sha256=RyLaiVPqMklSjbVKnriDk7MSIfSyetI6B1v8-ZILgv8,190
|
|
57
|
+
idp/static/idp/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2,sha256=Ixf6S7KTycCxEOGDFdUpI1xHoN3TM4zqPYx5VekniZ4,7420
|
|
58
|
+
idp/static/idp/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2,sha256=aJRDlpSUalidFX7OADCGlgpqQBPXSoE9q3YC79s9jAk,15084
|
|
59
|
+
idp/static/idp/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2,sha256=gk9IW10m4vLaPCsjYTLs4byOTkM3NFKVC7DkBUi0MT8,16512
|
|
60
|
+
idp/static/idp/assets/geist-latin-wght-normal-BgDaEnEv.woff2,sha256=GfnJJUaqMAwxIjXjElrxuBOU2NuaS8SkJc1bZB0tVOE,29400
|
|
61
|
+
idp/static/idp/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2,sha256=zYgAmZBwtynhzAv3pI2mw6wJagRCURcbmnV0tW2W1LQ,6176
|
|
62
|
+
idp/static/idp/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2,sha256=SGZ4f8lS29vVkdaSPWe8IcLYlLk_NKtp0M3yXUe_st8,12940
|
|
63
|
+
idp/static/idp/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2,sha256=GhieuZfD4uzmg3Pjh6-uyehhdCQYbEsas8_3xUumIjs,14696
|
|
64
|
+
idp/static/idp/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2,sha256=aErVtTH4HUPB6McDgmLV23zcH2gAbgTWx3ae-o0zyMw,23128
|
|
65
|
+
idp/static/idp/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2,sha256=W7Ztgxm6FgK7LrZ9zo15wrRoe-jOGDoFdEM-lJs8YK0,5812
|
|
66
|
+
idp/static/idp/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2,sha256=05tgiJqUpSen9zx5iKLW77bAgWFK72g7CzhsAtdMIXU,7696
|
|
67
|
+
idp/static/idp/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2,sha256=j6QOXSSCR3NeuXoL1ZO4hSRAQwYA1roBNkwx_gq8H-E,8004
|
|
68
|
+
idp/static/idp/assets/i18n-BZyjOlbu.js,sha256=lZ_fa1mJ08lnL7-3VeHQ7nIgwPKgSBbix9XwB6by3PY,21873
|
|
69
|
+
idp/static/idp/assets/i18n-Dog5o8z6.js,sha256=-I0bXPYB401JQcWqjnypZSohpKJmWKw05D2Wfxvk-4g,50
|
|
70
|
+
idp/static/idp/assets/roboto-latin-variable-ccAYIvAh.woff2,sha256=FATKNIvXXvg29N2LbyzHGUWGQtEjfDaClrL8ZS3KR9w,43136
|
|
71
|
+
idp/static/idp/assets/schemas-Bl_iabpv.js,sha256=cTP5DkER2DtU8cSec9zPLKJoScFiAx116wkyr85L1Ww,125041
|
|
72
|
+
idp/static/idp/assets/secure-account-Dl1pS_Wo.js,sha256=Z5NW_nQ6I0CmCcyvS_M5Ri5ABziltF8A8cfK2NGFW7o,6925
|
|
73
|
+
idp/static/idp/assets/service-BhG4-X2O.js,sha256=PoHAfvw57_DnpP5_RytoB_Mk9uFVemNRjecZoQFdGAg,55
|
|
74
|
+
idp/static/idp/assets/service-vCuiuvPs.js,sha256=MZgvMN7xMF2DJu7lT4yW8ALTRzwoHt3NrxaT8BXUOWg,2140
|
|
75
|
+
idp/static/idp/assets/theme-toggle-C07m3YIv.js,sha256=YsNcs25XbcgBJ1dkxEL60ntVS4oVKRLXZmmND3Qs6lI,42495
|
|
76
|
+
idp/static/idp/assets/two-step-section-DMQmuG2q.js,sha256=eE-VtR4B8yMs0z3GAAgeYmxj84xx7EgLxCiBy_aMC7o,13404
|
|
77
|
+
idp/static/idp/assets/webauthn-BQas4tAd.js,sha256=dNBLDbM5QRyWuMvbfIcNG774ovX37fGc6mCp6byKQBU,2036
|
|
78
|
+
idp/templates/shell.html,sha256=bpsGx60ROA-1KXOpABIGqtqbB37ujE89visBUwLrSnQ,839
|
|
79
|
+
idp/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
80
|
+
idp/web/admin.py,sha256=hjUAsbVC67xTjjWx3gJjRJwP5KQ3_GuEP_MDs5ZEm30,34579
|
|
81
|
+
idp/web/api.py,sha256=CvRXtTf0m-TyLnnwCkg6BzKW6rWwm1EtXQK_SDAOwag,25094
|
|
82
|
+
idp/web/developer.py,sha256=SpfZdKaqVNtQW06uA2spLoeuvctFV-JVGYxAnWo_7YE,24589
|
|
83
|
+
idp/web/oidc.py,sha256=g8Xlbvd1g7ZGOX9xZBXkqKhReXyXGLfdHeztCj7DPwU,15107
|
|
84
|
+
idp/web/openapi.py,sha256=49Kz3SCJVC0Fm3uebqvVG_wtpd1bm4AMFSm6dRmEayQ,41127
|
|
85
|
+
idp/web/pages.py,sha256=VrQTLAL2womm8VqXJi2ChnhTut4v0po9tWROQO5i9Xw,7900
|
|
86
|
+
idp/web/validation.py,sha256=5Ky2kZo2d41RbtFgnqsK1ptvZ7zwvrOpYJ_AF7CbFYM,1873
|
|
87
|
+
buntu-0.1.0.dist-info/METADATA,sha256=DUEUW7k-Ltyh4mHhzkaAmXlGW10KVajzZCKVaZMODQk,7473
|
|
88
|
+
buntu-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
89
|
+
buntu-0.1.0.dist-info/top_level.txt,sha256=JJAnI16L4ryMUzpMjs3enjf_HDoDpuKsOBu4PHxbSng,4
|
|
90
|
+
buntu-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ibrahima Touré
|
|
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 @@
|
|
|
1
|
+
idp
|
idp/__init__.py
ADDED
|
File without changes
|
idp/app.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""The application factory."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
|
|
6
|
+
from flask import Flask, Response, jsonify, request
|
|
7
|
+
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
8
|
+
|
|
9
|
+
from idp import cli, plugins
|
|
10
|
+
from idp.config import Settings, get_settings
|
|
11
|
+
from idp.db import db, init_db
|
|
12
|
+
from idp.extensions import limiter
|
|
13
|
+
from idp.oauth.server import init_oauth
|
|
14
|
+
from idp.services import backchannel, session_check
|
|
15
|
+
from idp.services.platform import PlatformError
|
|
16
|
+
from idp.web import admin, api, developer, oidc, openapi, pages
|
|
17
|
+
|
|
18
|
+
# Endpoints a browser application (SPA) calls cross-origin.
|
|
19
|
+
_CORS_PATHS = ("/oauth/token", "/oauth/revoke", "/oauth/userinfo")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_app(settings: Settings | None = None) -> Flask:
|
|
23
|
+
settings = settings or get_settings()
|
|
24
|
+
problems = settings.production_problems()
|
|
25
|
+
if problems:
|
|
26
|
+
raise RuntimeError("refusing to start:\n- " + "\n- ".join(problems))
|
|
27
|
+
# Authlib refuses plain HTTP; allowed only for an issuer on this machine (development, tests).
|
|
28
|
+
if settings.local:
|
|
29
|
+
os.environ["AUTHLIB_INSECURE_TRANSPORT"] = "1"
|
|
30
|
+
app = Flask(__name__)
|
|
31
|
+
if settings.trusted_proxies:
|
|
32
|
+
# The client's address and scheme as the proxies saw them, trusting only that many hops.
|
|
33
|
+
n = settings.trusted_proxies
|
|
34
|
+
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=n, x_proto=n) # type: ignore[method-assign]
|
|
35
|
+
app.config.update(
|
|
36
|
+
SECRET_KEY=settings.secret_key.get_secret_value(),
|
|
37
|
+
SESSION_COOKIE_NAME="idp_session",
|
|
38
|
+
SESSION_COOKIE_HTTPONLY=True,
|
|
39
|
+
SESSION_COOKIE_SAMESITE="Lax",
|
|
40
|
+
SESSION_COOKIE_SECURE=settings.secure_cookies,
|
|
41
|
+
PERMANENT_SESSION_LIFETIME=timedelta(seconds=settings.session_ttl),
|
|
42
|
+
RATELIMIT_STORAGE_URI=settings.rate_limit_storage,
|
|
43
|
+
RATELIMIT_ENABLED=settings.rate_limit_enabled,
|
|
44
|
+
)
|
|
45
|
+
app.json.sort_keys = False # type: ignore[attr-defined]
|
|
46
|
+
init_db(app, settings.database_url)
|
|
47
|
+
init_oauth(app)
|
|
48
|
+
limiter.init_app(app)
|
|
49
|
+
|
|
50
|
+
app.register_blueprint(oidc.bp)
|
|
51
|
+
app.register_blueprint(api.bp)
|
|
52
|
+
app.register_blueprint(pages.bp)
|
|
53
|
+
app.register_blueprint(admin.bp)
|
|
54
|
+
app.register_blueprint(developer.bp)
|
|
55
|
+
app.register_blueprint(openapi.bp)
|
|
56
|
+
_init_plugins(app, settings)
|
|
57
|
+
|
|
58
|
+
# A plugin's routes refuse through the same helpers as the developer and admin APIs
|
|
59
|
+
# (require_developer, member_of): answered as those APIs answer, not as a server fault.
|
|
60
|
+
@app.errorhandler(PlatformError)
|
|
61
|
+
def _platform_refusal(error: PlatformError) -> tuple[Response, int]:
|
|
62
|
+
db().rollback()
|
|
63
|
+
return jsonify(error=error.code, **error.details), error.status
|
|
64
|
+
|
|
65
|
+
cli.register(app)
|
|
66
|
+
|
|
67
|
+
# Logout tokens for the sessions this request ended, once it is answered.
|
|
68
|
+
app.after_request(backchannel.flush)
|
|
69
|
+
|
|
70
|
+
@app.after_request
|
|
71
|
+
def _headers(response: Response) -> Response:
|
|
72
|
+
headers = response.headers
|
|
73
|
+
headers.setdefault(
|
|
74
|
+
"Content-Security-Policy",
|
|
75
|
+
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; "
|
|
76
|
+
"img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; "
|
|
77
|
+
"base-uri 'none'; form-action 'self'",
|
|
78
|
+
)
|
|
79
|
+
# The session check page is meant to be framed by apps; nothing else is.
|
|
80
|
+
if request.path != "/oauth/check-session":
|
|
81
|
+
headers.setdefault("X-Frame-Options", "DENY")
|
|
82
|
+
session_check.sync_cookie(response)
|
|
83
|
+
headers.setdefault("X-Content-Type-Options", "nosniff")
|
|
84
|
+
headers.setdefault("Referrer-Policy", "no-referrer")
|
|
85
|
+
if settings.issuer.startswith("https://"):
|
|
86
|
+
# Not includeSubDomains: other services of the deployment's domain decide for themselves.
|
|
87
|
+
headers.setdefault("Strict-Transport-Security", "max-age=31536000")
|
|
88
|
+
# Avatars are public and their address changes with them: cached like static files.
|
|
89
|
+
if not request.path.startswith(("/static/", "/.well-known/", "/oauth/check-session.js", "/avatars/")):
|
|
90
|
+
headers["Cache-Control"] = "no-store"
|
|
91
|
+
origin = request.headers.get("Origin")
|
|
92
|
+
if origin and origin in settings.cors_origins and request.path in _CORS_PATHS:
|
|
93
|
+
headers["Access-Control-Allow-Origin"] = origin
|
|
94
|
+
headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"
|
|
95
|
+
headers["Access-Control-Allow-Methods"] = "GET, POST"
|
|
96
|
+
headers["Vary"] = "Origin"
|
|
97
|
+
return response
|
|
98
|
+
|
|
99
|
+
return app
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _init_plugins(app: Flask, settings: Settings) -> None:
|
|
103
|
+
"""Load the deployment's plugins, mount their APIs, and sync their catalogue if asked."""
|
|
104
|
+
loaded = plugins.load(settings.plugins)
|
|
105
|
+
plugins.use(loaded)
|
|
106
|
+
for plugin in loaded:
|
|
107
|
+
blueprint = plugin.blueprint()
|
|
108
|
+
if blueprint is not None:
|
|
109
|
+
app.register_blueprint(blueprint, url_prefix=f"/api/plugins/{plugin.id}")
|
|
110
|
+
if loaded and settings.plugins_sync_on_start:
|
|
111
|
+
from idp.plugins.sync import sync
|
|
112
|
+
|
|
113
|
+
with app.app_context():
|
|
114
|
+
sync(loaded)
|