Edouard Vanbelle
390aa31443
feat(cli): merge oxicloud binary and cli
...
this feature to simplify the creation of only 1 binary for multiple architecture
2026-08-29 11:57:48 +02:00
Edouard Vanbelle
2760fe9efc
refactor: rename tests on ref_count
2026-08-23 23:19:11 +02:00
Edouard Vanbelle
8d696ccc60
test(ref_count): check ref_count accross copy_folder and folder deletion
2026-08-23 23:19:11 +02:00
Edouard Vanbelle
811c7b0f12
feat(DPoP): add API test
2026-08-09 01:56:07 +02:00
Edouard Vanbelle
5418a5bd34
feat(opaque): add end to end test hurl + binary
2026-08-04 07:03:08 +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
8329b4aa56
feat(storage): improve admin panel
2026-08-01 17:10:06 +02:00
Edouard Vanbelle
394708c9a1
feat(recov. job): add drive consistency service
2026-07-29 01:25:15 +02:00
Edouard Vanbelle
4ab57d0d8e
test(job-registry): additional hurl coverage
2026-07-28 00:35:34 +02:00
Edouard Vanbelle
3b31b8911b
feat(breadcrumb): build breadcrumb in 1 API call
...
add /api/folders/{id}/ancestors
this API to iterate parent up to the drive root or the shared folder
this will help UI to build the breadcrumb in 1 API call
and to identify the root element (is it a drive users has access to or
a shared folder ?)
ui: now only 1 API call is now required to build the breadcrumb
2026-07-27 01:33:09 +02:00
Bradley Nelson
3ad38ddde7
test(mounts): admin CRUD hurl coverage + frontend mounts-tab tests
...
Close the two coverage gaps in the external-mounts feature — the admin
REST surface and the admin UI tab both previously had no automated tests.
Backend (hurl):
- tests/api/external_mounts.hurl — black-box coverage of the admin mount
endpoints (list / create / delete): input validation (empty name,
non-existent host path → 400), create → 201 with the full view, the
mount appears in the list, delete → 204 then 404, and the /api/admin
middleware denies a non-admin on every verb → 403.
- server.env: enable OXICLOUD_ENABLE_EXTERNAL_MOUNTS for the test server;
run.sh: register the new scenario.
Frontend (vitest):
- admin/page.test.ts — opening the mounts tab loads + lists mounts,
the create form calls createExternalMount, and delete goes through the
confirm modal to deleteExternalMount.
Tooling:
- devenv.nix: add b3sum (used by tests/api/run.sh's storage-integrity check).
2026-07-21 18:19:03 -06:00
M.Schmidt
ec2b533a53
security(webdav): adapt to anti-enum pattern
2026-07-19 19:55:26 +02:00
M.Schmidt
d57f7bfe3a
fix(webdav): wire orphaned PATCH tests + fix NC error-mapping and path bugs they caught
...
webdav_patch.hurl and nc_webdav_patch.hurl existed with real coverage
since the original PATCH commits but were never added to
tests/api/run.sh, so just api-test/CI silently skipped them. Wire both
in, fix nc_webdav_patch.hurl's header-after-[BasicAuth] ordering bug
that meant it had never actually passed, and add two new
consistency-focused files chaining PATCH operations with
cross-protocol/cross-surface verification:
- webdav_patch_consistency.hurl: chained overwrites with ETag-change
checks, GET/HEAD/PROPFIND cross-protocol agreement, quota-507
leaving the file byte-for-byte unchanged, direct_put_max_bytes
prefix/suffix regression coverage.
- nc_webdav_patch_consistency.hurl: Editor/Viewer/Outsider permission
matrix, cross-surface lock interop, quota-507 via the NC surface.
Running these surfaced two real bugs in the NC PATCH handler, both
fixed here:
- The write step mapped every error (including a legitimate anti-enum
permission denial) to a raw 500 instead of AppError::from(e), unlike
the plain surface. A Viewer without Update permission got a 500
leak instead of the expected 404.
- nc_to_internal_path() didn't strip the leading '/' that chroot.path
carries from StoragePath::to_string(), so a LOCK taken via /webdav/
silently failed to block PATCH via /remote.php/dav/ on the same
file — the exact-string lock-store lookup never matched. Added a
regression unit test.
2026-07-19 10:35:35 +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
e0156a43f5
security(wopi): resolve PutFile drive_id from file, not caller's default
2026-07-17 20:43:42 +02:00
Dionisio Pozo
b69c18b934
Merge pull request #598 from EdouardVanbelle/feat/readonly-drive
2026-07-16 07:40:29 +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
53c301e472
fix(595): permit unlimited user quota
2026-07-15 22:21:51 +02:00
Dionisio Pozo
e0b9e23201
Merge pull request #591 from EdouardVanbelle/fix/caldav-time-range-parser
...
fix(ical): fix timerange issue
2026-07-15 07:22:14 +02:00
Dionisio Pozo
fc5a3a581f
Merge pull request #592 from EdouardVanbelle/fix/carddav-parser-tel-adr
...
fix(carddav): fix tel uri
2026-07-15 07:21:57 +02:00
Edouard Vanbelle
a67fcadeea
fix(ical): fix timerange issue
...
accept iCal DATE-TIME + RFC 3339 fallback
2026-07-14 23:13:50 +02:00
Edouard Vanbelle
dc2c8f5dcd
fix(carddav): fix tel uri
2026-07-14 23:00:19 +02:00
Edouard Vanbelle
7966c7178a
fix(528): pass3: PUT with RECURRENCE-ID
2026-07-14 20:42:18 +02:00
Edouard Vanbelle
a7a45b3383
fix(caldav+carddav): raise 400 error on param issue
...
rather than a 500
2026-07-14 14:52:58 +02:00
Edouard Vanbelle
54b5b3bf4f
feat(caldav+carddav): auto create default cal & card
...
automatically create default Calendar and default addressbook per user
(no creation if user already have a such resource)
default name are "Personal"
this is using the user's life cycle like does the drives
answers to issue #545
2026-07-14 14:30:05 +02:00
Dionisio Pozo
970f97b91a
Merge pull request #570 from swissiety/rfc-4331-quota-properties
...
feat(webdav): RFC 4331 quota-available-bytes/quota-used-bytes
2026-07-14 12:58:22 +02:00
Edouard Vanbelle
f331dbf0ee
feat(account): upgrade external to internal
2026-07-14 11:10:23 +02:00
Dionisio Pozo
5ae551a93d
Merge pull request #577 from EdouardVanbelle/feat/users-perfs-and-filter-dotfiles
...
feat: users prefs server side + filter dotfiles + filter shares by resource type
2026-07-14 09:07:09 +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
M.Schmidt
6701ddfc17
Merge branch 'main' into rfc-4331-quota-properties
...
# Conflicts:
# src/interfaces/nextcloud/report_handler.rs
# src/interfaces/nextcloud/webdav_handler.rs
# tests/api/run.sh
2026-07-13 20:32:01 +02:00
Edouard Vanbelle
5aaf49859e
feat(user-perf): add ui user-perf + dotfile filter
...
- add resource kind filter (file, folder, drive) in shared section (localStorage stored)
- add user preferences serverside store
- add client side dotfile filter (show/hide dotfiles) (user perf stored, default: dotfiles are shown)
for security trashed dotfile are always displayed
protection added: if a folder has only hidden items, a notification invite user to display it
if a user rename or create a hidden item, a notification tells it to user
2026-07-13 20:27:52 +02:00
Edouard Vanbelle
05ef55a8e0
fix(nc): login OIDC + drive picker
...
ensure OIDC is supported during nextcloud login
flow is:
1. nextcloud
2. oxicloud login ( direct pass or OIDC according config)
3. drive picker (if user has multiple drive)
4. success page + backchannel login to nextcloud
2026-07-13 18:30:20 +02:00
Dionisio Pozo
06da428493
Merge pull request #572 from EdouardVanbelle/feat/nextcloud-chrooted-drive
...
feat/nextcloud chrooted drive
2026-07-13 09:37:10 +02:00
Dionisio Pozo
22e09c09be
Merge pull request #538 from swissiety/webdav-litmus-compliance
...
implement dead properties for nextcloud handler and fixup frontend migration leftover
2026-07-13 09:36:43 +02:00
M.Schmidt
c07aeabd85
feat(webdav): drive-aware RFC 4331 quota properties
...
resolve_quota only ever reported the caller's personal envelope,
ignoring the drive_id already resolved at every PROPFIND call site —
shared drives with their own quota showed the wrong numbers. Adds
AppState::resolve_webdav_quota, shared by both WebDAV surfaces:
nil drive_id or personal drive -> account envelope, shared drive ->
its own storage.drives quota/used_bytes.
Also adds quota-used-bytes/quota-available-bytes to the NextCloud-
compatible surface, which previously had no RFC 4331 support at all.
Registers webdav_quota_properties.hurl and the new
nc_webdav_quota_properties.hurl in tests/api/run.sh — neither was
wired into the suite before this change.
2026-07-13 00:34:17 +02:00
M.Schmidt
7011fdff5a
Merge origin/main into webdav-litmus-compliance
2026-07-12 22:25:12 +02:00
Edouard Vanbelle
52814b4d7c
fix(nextcloud): fix chroot + synchronisation
...
- add better hurl coverage on nextcloud chrooted login
- fix issue with nextcloud using /{drive name}/~{drive id}/
- fix trashbin handler
confusion username vs {username}~{folder id}
2026-07-12 22:19:45 +02:00
Edouard Vanbelle
ba620166ee
feat(grant): clean up expired grants
2026-07-12 18:37:13 +02:00
Edouard Vanbelle
c1e46910b0
feat(music): move playlist to authz engine
2026-07-08 01:04:03 +02:00
Edouard Vanbelle
a2ad7757c3
feat(calendar,addressbook): add tests for authz
2026-07-08 01:03:25 +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
Edouard Vanbelle
75601beb43
security(wopi): add authz to Wopi
2026-07-05 23:31:44 +02:00
M.Schmidt
3501857a70
test(webdav): cover protected-property PROPPATCH rejection
...
Native + NC surfaces: DAV: displayname/getetag, oc:fileid/
permissions, nc:has-preview all 403 and never land in the store;
mixed request shows per-property granularity (protected prop 403
alongside an ordinary custom prop 200); oc:favorite regression
guard confirms its special-case still works despite being on the
protected list.
2026-07-01 22:54:56 +02:00
M.Schmidt
94e0145855
test(webdav): cover NC dead-props PROPPATCH/PROPFIND contract
...
Round-trip, upsert, remove, 404 on missing resource, survives MOVE,
reaped on DELETE, oc:favorite regression guard, folder coverage.
2026-07-01 19:59:23 +02:00
Edouard Vanbelle
cf1479d113
test(webdav): cover nested folder move
...
this test is a duplicated with the move in litmus test suite
it has been added because not everybody do have litmus installed
and no clue if litmus will be kept in the future
2026-06-30 22:55:49 +02:00
Edouard Vanbelle
21ac3a178f
test(webdav): cover the dead properties
2026-06-30 20:32:16 +02:00
Edouard Vanbelle
583af517c2
fix(drive): fix drive_id on copy to other drive
...
- fix alsot drive_id exposition
2026-06-29 23:21:17 +02:00
Edouard Vanbelle
ee92d365b9
feat(drive): ensure drive_id updated on file|folder moved to another drive
2026-06-29 21:11:40 +02:00
Edouard Vanbelle
ddb131da8b
feat(drive): add policy forbid_public_links
2026-06-26 13:59:02 +02:00
Edouard Vanbelle
cfd783cbd3
feat(drive): personal drives have the user's quota in commun
2026-06-26 13:59:02 +02:00