Edouard Vanbelle
7fc68c50d5
feat(DPoP): add schema & session & PG repos
2026-08-09 01:56:07 +02:00
Edouard Vanbelle
e9495a63ad
feat(oidc): permit auto/manual oidc account link/unlink
...
link are checking that email matches, +email alias are normalize into email
if email is already used on another account, link is not possible
not usurpation risk as the IDP is choosen by the admin
2026-08-08 19:21:13 +02:00
Edouard Vanbelle
d8b3f2e026
refactor(oidc): migrate provider into issuer
...
this make OIDC compliant with the invariant binding (issuer and subject)
admin can now rename their provider without breaking
clarifing federation_kind: report the kind of federation wired not the allowed login method
hybryd login method are still allowed
2026-08-08 16:37:45 +02:00
Edouard Vanbelle
10a8dd7d8b
refactor(oidc): prep. support of Open Cloud Mesh
...
add federation kind (OCM, OIDC, MagicLink)
rename oidc_provider into federation_issuer
rename oidc_subject into federation_subject
2026-08-08 15:10:26 +02:00
Edouard Vanbelle
60cf9d976b
feat(opaque): prepare removal of Argon legacy password for the future
2026-08-05 23:14:37 +02:00
Edouard Vanbelle
c4bf2568ba
feat(opaque): lower KSF values to support old clients (memory intensive)
2026-08-05 20:48:55 +02:00
Edouard Vanbelle
6965855388
reactor(opaque): normalize to OXICLOUD_AUTH_OPAQUE_* variables
2026-08-04 07:31:32 +02:00
Edouard Vanbelle
0e395ae15f
feat(auth): bring opaque (RFC 9807) auth
...
OPAQUE (RFC 9807) implementation (using `opaque-ke` crate)
with opaque authentfication, server will never receive the password (in the auth=password mode)
this is a must have to create trust with users to permit end to end encryption in the future
(we cannot know if user use the same password/passphrase for his asymetric key or his oxicloud auth,
this is why server must never have the password)
pass1: prepare server
2026-08-04 07:03:08 +02:00
Edouard Vanbelle
d065f99952
feat(oidc): deprecate OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN
2026-08-03 22:26:46 +02:00
Edouard Vanbelle
acd4420fe3
feat(oidc): impl back channel logout
2026-08-03 08:00:13 +02:00
Edouard Vanbelle
166b8c4891
feat(oidc): RP initiator logout
...
request token invalidation to IdP (OIDC) on logout
2026-08-03 07:59:14 +02:00
Edouard Vanbelle
5ebe2d3bae
feat(oidc): add auto-redirect for OIDC
...
add `auto_redirect_if_standalone_oidc` in `OXICLOUD_AUTH_POLICIES`
let admin decide to redirect immediately to IdP if OIDC is the only auth method enabled
2026-08-03 01:01:00 +02:00
Edouard Vanbelle
b91f2fab2b
feat(oidc): add oidc method in OXICLOUD_AUTH_METHODS
...
permit an admin to specify `oidc` only as the only method to login/register
note that if OIDC is enabled, the engine always append oidc in OXICLOUD_AUTH_METHODS
we could move to an explicit declaration in a major release
2026-08-03 00:49:40 +02:00
Edouard Vanbelle
7663f803d3
fix: fix services accessig directly to localstorage
...
Prevent services accessing directly to localstorage and prefer using an astraction layer
to expose full blob. The abstraction layer (dedup services) will cover backend storage
election (local, s3, ...), encryption, etc
This change permit audio_metadata_service, media_metadaa_service, face_indexing_service to handle
blobs without worring of the backend.
note: prefered way to handle blob is the streamed way. Some services may not have this possibility
2026-08-02 22:20:46 +02:00
Edouard Vanbelle
6ab87d83e8
docs(backend-storage): explain design including the blob header
2026-08-02 17:38:47 +02:00
Edouard Vanbelle
015f2da0f7
refactor(backend): normalize naming convention to backend rather storage
...
no ambiguity with the backend rather storage
2026-08-02 14:56:29 +02:00
Edouard Vanbelle
9902a6f8fe
refactor(usage_reconcile): explicit naming to prevent confusion with storage (backend)
2026-08-02 14:56:29 +02:00
Edouard Vanbelle
e164689771
feat(storage key rot): remove dead born OXICLOUD_STORAGE_<N>_ENCRYPTION_CIPHER
...
+ alway ovewrite on storage migration (got issue when migrating with blob already existing and a key change)
2026-08-02 02:40:33 +02:00
Edouard Vanbelle
4cb73eaf39
plan(storage-key-rotation): add a key rotation + header version blob
2026-08-02 02:40:33 +02:00
Edouard Vanbelle
142afecbbf
feat(storage): add a guide on storage
2026-08-01 17:50:35 +02:00
Edouard Vanbelle
8329b4aa56
feat(storage): improve admin panel
2026-08-01 17:10:06 +02:00
Edouard Vanbelle
2de5abc6ca
plan(storage-multi-entry): simplify the storage migration
...
Two chronic problems fall out:
1. **Split-brain config.** Admin edits DB via the panel; app boot ignores DB.
Migration completes; live backend hasn't moved. Admin has to remember to
copy env vars into `.env` and restart. Two sources of truth for the same
setting. Cutover is a manual multi-step flow; users routinely get it wrong.
2. **Migration data-loss window on concurrent writes.** The copy walks
`storage.blobs` in hash order. A blob whose hash is lex-lower than the
current cursor, written to source AFTER migration passed it, is never
copied to target. `passed=true, findings=0` completion does NOT guarantee
target has every blob. Silent.
3. **Migration target selection is fragile.** DTO passes the whole S3 config
at trigger time; secrets sit plaintext in `admin_settings`. Any future
pluggable-storage story compounds this (Azure, GCS, WebDAV-as-source, …).
This plan replaces the split-brain model with a single-source-of-truth
architecture:
- `.env` declares **N named storage entries** (immutable per-deploy).
- `admin_settings.storage.active_backend_name` holds ONE row — which named
entry the app currently runs on. That's the whole runtime config.
- Migration is the atomic transition from one active entry to another. Server
is put in read-only mode for the copy window; on completion, the active
pointer flips; a restart cuts over.
2026-08-01 12:05:01 +02:00
Edouard Vanbelle
0f12399a48
feat(recoverable-job): fix files_consistency to check blob chunk consistency
2026-07-29 22:17:15 +02:00
Edouard Vanbelle
5881968f50
feat(recoverable-job): add progress view
2026-07-29 08:41:19 +02:00
Edouard Vanbelle
e1556e3d36
feat(recoverable-job): add findings
2026-07-29 07:57:06 +02:00
Edouard Vanbelle
41d83b3053
feat(recoverable-job): add consistency_batch (runs all consistency check)
2026-07-29 01:44:52 +02:00
Edouard Vanbelle
782a5c99bd
feat(recoverable-job): add folder_consistency
2026-07-29 01:39:58 +02:00
Edouard Vanbelle
b343ab5e0e
plan(job): clarify way to split consistency job
2026-07-29 01:22:12 +02:00
Edouard Vanbelle
0b7618d858
clarify naming conventions
2026-07-28 22:00:28 +02:00
Edouard Vanbelle
996cb98a6d
doc(job-registry): add doc for implementors
2026-07-28 21:24:09 +02:00
Edouard Vanbelle
0e8b1fbbeb
refactor(job-registry): simplify the job registering*
2026-07-28 21:13:09 +02:00
Edouard Vanbelle
f66f7fa31f
feat(job-registry): remplace /api/admin/internal/trigger-*
...
remplace /api/admin/internal/trigger-* to /api/admin/jobs/{...}/trigger
remove OXICLOUD_ENABLE_ADMIN_INTERNAL_ENDPOINTS
2026-07-27 23:54:43 +02:00
Edouard Vanbelle
dfedde54a4
feat(job-registry): wire /api/admin/jobs/*
2026-07-27 23:38:12 +02:00
Edouard Vanbelle
f5f794fde5
feat(job-registry): handle job without periodicity but with trigger
2026-07-27 22:53:13 +02:00
Edouard Vanbelle
5e8d894a0c
chore(plan): add job-registry + consistency check
...
purpose is to design a job registry with a scheduler
thi aim to drive in the same way any services requiring execution of periodic background tasks
plugins could benefeciate it
purpose is mostly to add a normative way to implement consistency check per services
this is to edutcate implementors adding any new services
goal is to ensure data quality with oxicloud and resumable jobs by default
2026-07-27 21:53:00 +02:00
Edouard Vanbelle
87ddf8ffc8
refactor(search): simplify order by + wire reverse
2026-07-26 18:18:15 +02:00
Edouard Vanbelle
c22741bc7f
refactor(search): normalize answer to /resources format
2026-07-26 18:18:15 +02:00
Edouard Vanbelle
5982efd783
i18n(drive): correct locales for drive sections
2026-07-19 16:26:07 +02:00
Edouard Vanbelle
c2b5d9fe2e
security(/api/dedup): normalize dedup admin routes into /api/admin
...
/dedup/stats -> /api/admin/dedup/stats
/dedup/recalculate -> /api/admin/dedup/recalculate
2026-07-17 21:51:48 +02:00
Edouard Vanbelle
dd72b77c22
security(search): move DELETE /search/cache to protected path
2026-07-17 19:01:05 +02:00
Edouard Vanbelle
a6427fc028
feat(drive): add readonly policy
...
permmit admin to freeze a drive, trash janitor background job is also disabled for this drive
2026-07-16 01:02:15 +02:00
Edouard Vanbelle
5e95d6dccf
doc: update doc to reflect recent changes
...
- grants: permission moved to roles
- new resources (Drive, Caldav, Carddav, Playlist) now using ReBAC
- expired shared now cleaned up
- drive visible in Webdav
- new login/registration options (domain allow list, policies, etc)
- upgrade of external user into internal user
2026-07-14 13:31:48 +02:00
Dionisio Pozo
3ecfddd6b8
Merge pull request #583 from EdouardVanbelle/doc/app-password
...
doc(app password): correct doc: webdav login via app password
2026-07-14 12:58:06 +02:00
Edouard Vanbelle
ebb11f19c2
doc(webdav): login is via app password
2026-07-14 12:21:51 +02:00
Edouard Vanbelle
33d0c460fd
chore(vitepress): convert ../ references to github/DioCrafts/OxiCloud links
...
unblock site generation and link code reference to github DioCrafts/OxiCloud project
2026-07-14 12:02:12 +02:00
Edouard Vanbelle
e94063d96a
test(login/register): via password or magic-link
...
Password login
┌─────┬────────────────────────────────────────────────────┬────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────┐
│ # │ Case │ Where │ Assertion │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L1 │ Login by username │ auth_login.hurl Case 1 │ 200 + access_token, user.email match │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L2 │ Login by email (dispatch on @) │ auth_login.hurl Case 2 │ 200, same session shape as L1 │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L3 │ Bad password on username path │ auth_login.hurl Case 3 │ 403 anti-enum │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L4 │ Bad password on email path │ auth_login.hurl Case 4 │ 403 anti-enum (same shape as L3) │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L5 │ Unknown username │ auth_login.hurl Case 5 │ 403 anti-enum (same shape as L3) │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L6 │ Unknown email │ auth_login.hurl Case 6 │ 403 anti-enum (same shape as L3) │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L7 │ /api/auth/oidc/providers reports methods correctly │ auth_login.hurl Case 7 │ password_login_enabled: true, magic_link_login_enabled: true, require_verified_email: false │
└─────┴────────────────────────────────────────────────────┴────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────┘
Password registration
┌─────┬───────────────────────────────────────────────────┬──────────────────────────────┬─────────────────────────────────────────────────────────┐
│ # │ Case │ Where │ Assertion │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────────┤
│ R1 │ Classic username + email + password → uniform 200 │ registration.hurl Step 2 │ anti-enum message contains "request received" │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────────┤
│ R2 │ Login after register works │ registration.hurl Step 2b │ 200 + session for the new user │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────────┤
│ R3 │ Email collision → uniform 200 (no rewrite) │ registration.hurl Steps 8-10 │ attacker password doesn't work; original account intact │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼────────────────────────────┤
│ R4 │ Username collision → uniform 200 │ registration.hurl Step 11 │ same anti-enum shape │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼────────────────────────────┤
│ R5 │ Off-domain rejection │ registration.hurl Step 12 │ 403 RegistrationDomainNotAllowed │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼────────────────────────────┤
│ R6 │ Case-insensitive domain match │ registration.hurl Step 12b │ uniform 200 on charlie@EXAMPLE.COM │
└─────┴───────────────────────────────────────────────────┴──────────────────────────────┴────────────────────────────┘
Magic-link registration (email-only signup)
┌─────┬──────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────┐
│ # │ Case │ Where │ Assertion │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR1 │ Email-only signup → welcome mail queued │ registration.hurl Step 3 │ uniform 200 + browser-binding cookie set │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR2 │ Welcome mail contains magic-link URL │ registration.hurl Step 4 │ captured from mock SMTP │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR3 │ PR 22 cross-browser confirmation page │ registration.hurl Step 5a │ 200 HTML "different browser" │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR4 │ Cookie-bound redemption lands on SPA │ registration.hurl Step 5b │ 302 → /files (SvelteKit route, post-migration) │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR5 │ email_verified_at stamped after redemption │ registration.hurl Step 6 │ field present on /api/auth/me │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR6 │ Second magic-link post-signup │ registration.hurl Step 7 │ uniform 200 │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR7 │ Profile PATCH — no-op, name set, empty-string rejected, username-taken 409, claim-once 409, etc. │ registration.hurl Steps 6a–6i │ full profile lifecycle │
└─────┴──────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────┘
Magic-link login (existing account)
┌─────┬──────────────────────────────────────────────────────────┬──────────────────────────────────────┬───────────────────────────────────────┐
│ # │ Case │ Where │ Assertion │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML1 │ Baseline password login still works │ auth_magic_link_login.hurl Steps 1-2 │ 200 │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML2 │ magic-link/send with email identifier │ auth_magic_link_login.hurl Step 3 │ uniform 200 + cookie │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML3 │ magic-link/send with username identifier (dispatch on @) │ auth_magic_link_login.hurl Step 4 │ uniform 200 │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML4 │ Password-user policy: mail actually sent │ auth_magic_link_login.hurl Step 5 │ SMTP capture proves permit_magic_link_for_password_users in effect │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML5 │ Redemption creates a session │ auth_magic_link_login.hurl Steps 6-7 │ 302 → /files, /api/auth/me returns the same user │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML6 │ Anti-enum on unknown identifier │ auth_magic_link_login.hurl Step 8 │ same uniform 200 shape as ML3 │
└─────┴──────────────────────────────────────────────────────────┴──────────────────────────────────────┴───────────────────────────────────────┘
OIDC
┌─────┬────────────────────────────────────────────────────────────────────────┬───────────────────┬────────────────────────────────────────────────────────────────────────────────────────────┐
│ # │ Case │ Where │ Assertion │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O1 │ Setup local admin (bootstrap) │ oidc.hurl Step 1 │ 201 │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O2 │ Providers endpoint — OIDC visible │ oidc.hurl Step 2 │ enabled: true, provider_name: MockSSO, password_login_enabled: true, magic_link_login_enabled: false (OIDC-master rule) │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O2b │ Magic-link/send refused (endpoint layer) │ oidc.hurl Step 2b │ 403 MagicLinkLoginDisabled — proves the policy gate fires, not a 503 SMTP-unwired │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O3 │ Authorize redirect includes PKCE + state │ oidc.hurl Step 3 │ 307 to fake IdP │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O4 │ IdP round-trip + JIT provisioning │ oidc.hurl Step 4 │ Callback lands on /login?oidc_code=… │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O5 │ Code exchange → session cookies │ oidc.hurl Step 5 │ 200 + all three cookies │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O6 │ JIT profile mapping (name, given/family, picture, groups → admin role) │ oidc.hurl Step 6 │ every claim reflected on /api/auth/me │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O7 │ Refresh rotation on OIDC session │ oidc.hurl Step 7 │ new access/refresh/CSRF cookies │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O8 │ Refreshed cookies authenticate │ oidc.hurl Step 8 │ 200 on /api/auth/me │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O9 │ Repeat login = same local user (no dup) │ oidc.hurl Step 9 │ user_id stable │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O10 │ Anti-takeover: unverified email → refused │ oidc.hurl Step 10 │ 401/403 │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O11 │ One-time code replay refused │ oidc.hurl Step 11 │ second /exchange → 401 │
└─────┴────────────────────────────────────────────────────────────────────────┴───────────────────┴────────────────────────────────────────────────────────────────────────────────────────────┘
test
2026-07-14 03:16:25 +02:00
Edouard Vanbelle
01da450cf6
feat(registration): add a domain allow list
...
add:
- OXICLOUD_REGISTRATION_ALLOWED_EMAIL_DOMAINS to specify list of domains allowing a self registration
- OXICLOUD_REQUIRE_VERIFIED_EMAIL=true|false
- OXICLOUD_AUTH_METHODS=password,magic_link (login methods, OIDC is on top of this)
- OXICLOUD_AUTH_POLICIES=permit_magic_link_for_password_users (OIDC is on top)
2026-07-14 02:43:39 +02:00
Edouard Vanbelle
fe66a379b7
docs(drive): add drive doc
...
documentation for users
2026-07-13 19:16:23 +02:00
Edouard Vanbelle
ba620166ee
feat(grant): clean up expired grants
2026-07-12 18:37:13 +02:00
Edouard Vanbelle
7e34045ff8
feat(drive): fix webdav back-compat
...
add env variable `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX`
which is by default:
`OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX="@drive"`
so `/webdav/` -> points to user's personal drive (**backward compatibilit**y)
`/web/dav/@drive/{uuid|drive name}/` points to the respective drive
if admins want directly `/webdav/` pointing to list of drives they need to:
`OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX=""`
+ ensure lock is per user (RFC 4918 §9.11)
fix : #554
2026-07-06 22:14:50 +02:00