test(e2e): webdav + nextcloud full e2e test coverage
add a full coverage of Webdav and Nextcloud
purpose: prepare move to Drives and ensure no regression at all
test scenarios are in docs/plan/BASELINE_TESTS_NC_WEBDAV.md
current existing bugs identified via these tests:
┌──────────┬─────────┬────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Bug │ Surface │ Pin location │
├──────────┼─────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ G4/G5/K5 │ NC │ AlreadyExists → 500 instead of 412 (handle_move + trashbin restore) │
├──────────┼─────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ G9 │ NC │ Folder DELETE not row-recursive — orphan descendants stay live │
├──────────┼─────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ M5/M7 │ Native │ resolve_path_for_user mismatch — PUT writes, GET reads via lenient lookup, MOVE/DELETE can't find │
│ │ │ via strict │
├──────────┼─────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ M8 │ Native │ COPY discards destination filename — collides with source │
├──────────┼─────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ N2 │ Native │ LOCK creates the token, mutators don't check it — class-2 advertisement is aspirational │
└──────────┴─────────┴────────────────────────────────────────────────────────────────────────────────────────────────────┘
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
# =============================================================
|
||||
# OxiCloud — Baseline: NC Basic Auth failure modes
|
||||
# =============================================================
|
||||
# Group P from BASELINE_TESTS_NC_WEBDAV.md.
|
||||
#
|
||||
# Coverage:
|
||||
# P1 — no Authorization header → 401 + WWW-Authenticate
|
||||
# P2 — wrong password (real user) → 401
|
||||
# P3 — N wrong attempts from same IP against a THROWAWAY
|
||||
# username trip the per-(account,IP) lockout
|
||||
# P4 — per-IP lockout scope (the #323 regression guard):
|
||||
# 6 wrong attempts from spoofed X-Forwarded-For: IP1
|
||||
# lock (admin, IP1), but the SAME correct credential
|
||||
# from spoofed X-Forwarded-For: IP2 still succeeds.
|
||||
# Depends on OXICLOUD_TRUST_PROXY_HEADERS=true in
|
||||
# tests/common/server.env so the server honours the
|
||||
# X-Forwarded-For header on localhost.
|
||||
#
|
||||
# Deliberately NOT covered here:
|
||||
# P5 — External user attempts NC Basic Auth. Externals can't
|
||||
# mint app passwords in the first place (the upstream gate
|
||||
# is asserted in tests/api/external_users.hurl), so the
|
||||
# in-middleware belt-and-braces check is unreachable via
|
||||
# a black-box HTTP test. Verified by code inspection.
|
||||
# =============================================================
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Setup 1 — JWT login (needed to mint the app password for P4's
|
||||
# positive control).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "{{username}}", "password": "{{password}}" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
jwt: jsonpath "$.access_token"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Setup 2 — Mint admin's NC app password.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/app-passwords
|
||||
Authorization: Bearer {{jwt}}
|
||||
Content-Type: application/json
|
||||
{ "label": "nc_auth_failures P4 positive control" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
nc_username: jsonpath "$.username"
|
||||
nc_password: jsonpath "$.password"
|
||||
ap_id: jsonpath "$.id"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# P1 — PROPFIND without any Authorization header
|
||||
# → 401 with `WWW-Authenticate: Basic realm="OxiCloud"`.
|
||||
# NC desktop relies on this challenge to know it should
|
||||
# offer credentials at all.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
|
||||
HTTP 401
|
||||
[Asserts]
|
||||
header "WWW-Authenticate" contains "Basic"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# P2 — Wrong password against a real user
|
||||
# → 401 with the same WWW-Authenticate challenge.
|
||||
# Anti-enumeration: response shape identical whether the
|
||||
# user exists or not.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
[BasicAuth]
|
||||
{{username}}: definitely-wrong-password
|
||||
|
||||
HTTP 401
|
||||
[Asserts]
|
||||
header "WWW-Authenticate" contains "Basic"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# P3 — Lockout trip
|
||||
#
|
||||
# Consecutive bad-credential attempts against a throwaway
|
||||
# username trip the per-(account, IP) lockout. The default
|
||||
# threshold is 5 (`OXICLOUD_LOCKOUT_MAX_FAILURES = 5`) and
|
||||
# `tests/common/server.env` does NOT override it — the only
|
||||
# raised values in that env are the broader login / refresh /
|
||||
# register rate-limits, which prevent the rate-limiter from
|
||||
# firing AHEAD of the lockout under sustained test traffic.
|
||||
# The lockout itself still engages on the 6th attempt.
|
||||
#
|
||||
# Once engaged, every subsequent attempt for the same
|
||||
# (account, IP) pair short-circuits to 401 from the lockout
|
||||
# check, BEFORE app-password verification runs. The audit log
|
||||
# fires `WARN account_temporarily_locked` from
|
||||
# `login_lockout_service.rs` when the cap is hit — useful
|
||||
# correlation signal during a real run.
|
||||
#
|
||||
# A throwaway username (`nc-lockout-probe-…`) is used because
|
||||
# the lockout is keyed by (username, IP); locking a throwaway
|
||||
# pair never poisons admin's auth path, so downstream Hurl
|
||||
# tests in run.sh that authenticate as admin keep working.
|
||||
#
|
||||
# Limitation of a black-box HTTP probe: the wire response is
|
||||
# 401 with the same WWW-Authenticate header whether the 401
|
||||
# comes from "lockout engaged" or "still just rejecting bad
|
||||
# creds" — both look the same on the wire. Verification of
|
||||
# the lockout-engaged branch specifically lives in the unit
|
||||
# tests (`login_lockout_service.rs::tests`). What this Hurl
|
||||
# test guards is that 7 consecutive attempts keep returning
|
||||
# the same 401 shape (no 500s, no header drift), and the
|
||||
# server-side audit log confirms the lockout engaged at 5.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
||||
[BasicAuth]
|
||||
nc-lockout-probe-001: bad-1
|
||||
|
||||
HTTP 401
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
||||
[BasicAuth]
|
||||
nc-lockout-probe-001: bad-2
|
||||
|
||||
HTTP 401
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
||||
[BasicAuth]
|
||||
nc-lockout-probe-001: bad-3
|
||||
|
||||
HTTP 401
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
||||
[BasicAuth]
|
||||
nc-lockout-probe-001: bad-4
|
||||
|
||||
HTTP 401
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
||||
[BasicAuth]
|
||||
nc-lockout-probe-001: bad-5
|
||||
|
||||
HTTP 401
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
||||
[BasicAuth]
|
||||
nc-lockout-probe-001: bad-6
|
||||
|
||||
HTTP 401
|
||||
|
||||
# 7th attempt: account+IP is now locked at the middleware level.
|
||||
# Continues to return 401.
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
||||
[BasicAuth]
|
||||
nc-lockout-probe-001: bad-7
|
||||
|
||||
HTTP 401
|
||||
[Asserts]
|
||||
header "WWW-Authenticate" contains "Basic"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# P4 — Per-IP lockout scope (#323 regression guard)
|
||||
#
|
||||
# Goal: prove that locking out (admin, IP1) does NOT lock out
|
||||
# (admin, IP2) — the lockout is keyed by *both* parts, not by
|
||||
# username alone. This was the gap the reporter demonstrated:
|
||||
# an attacker spoofing X-Forwarded-For could lock a legitimate
|
||||
# user out from their own IP. The fix scoped the key.
|
||||
#
|
||||
# Mechanic:
|
||||
# 1. Pre-check: admin's app password works from the test's
|
||||
# default client IP (127.0.0.1, no X-Forwarded-For).
|
||||
# 2. 6 wrong attempts with X-Forwarded-For: 10.0.0.1 trip the
|
||||
# lockout for (admin, 10.0.0.1).
|
||||
# 3. CORRECT app password with X-Forwarded-For: 10.0.0.1 →
|
||||
# 401 (still locked from THIS IP — positive demonstration
|
||||
# that the lockout actually engaged, not just chance).
|
||||
# 4. CORRECT app password with X-Forwarded-For: 10.0.0.2 →
|
||||
# 207 (NOT locked from this IP — the load-bearing
|
||||
# assertion of P4).
|
||||
#
|
||||
# Why this doesn't break the rest of the suite: every other
|
||||
# test runs from the default client IP (127.0.0.1) without
|
||||
# X-Forwarded-For, so (admin, 127.0.0.1) is untouched. The
|
||||
# lockouts placed here are on (admin, 10.0.0.1) and
|
||||
# (admin, 10.0.0.2 — released by the success), neither of
|
||||
# which any other test touches.
|
||||
#
|
||||
# Requires: OXICLOUD_TRUST_PROXY_HEADERS=true in
|
||||
# tests/common/server.env.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Pre-check: app password works with no X-Forwarded-For ───
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
|
||||
HTTP 207
|
||||
|
||||
|
||||
# ── Step 1: burn the lockout for (admin, 10.0.0.1) ──────────
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
X-Forwarded-For: 10.0.0.1
|
||||
[BasicAuth]
|
||||
{{username}}: p4-bad-1
|
||||
|
||||
HTTP 401
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
X-Forwarded-For: 10.0.0.1
|
||||
[BasicAuth]
|
||||
{{username}}: p4-bad-2
|
||||
|
||||
HTTP 401
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
X-Forwarded-For: 10.0.0.1
|
||||
[BasicAuth]
|
||||
{{username}}: p4-bad-3
|
||||
|
||||
HTTP 401
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
X-Forwarded-For: 10.0.0.1
|
||||
[BasicAuth]
|
||||
{{username}}: p4-bad-4
|
||||
|
||||
HTTP 401
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
X-Forwarded-For: 10.0.0.1
|
||||
[BasicAuth]
|
||||
{{username}}: p4-bad-5
|
||||
|
||||
HTTP 401
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
X-Forwarded-For: 10.0.0.1
|
||||
[BasicAuth]
|
||||
{{username}}: p4-bad-6
|
||||
|
||||
HTTP 401
|
||||
|
||||
|
||||
# ── Step 2: CORRECT app password from IP1 — still 401 ──────
|
||||
# This is the positive demonstration that the lockout engaged.
|
||||
# If this were 207, the lockout would not have fired and the
|
||||
# subsequent IP2 success wouldn't prove anything.
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
X-Forwarded-For: 10.0.0.1
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
|
||||
HTTP 401
|
||||
|
||||
|
||||
# ── Step 3: CORRECT app password from IP2 — 207 ────────────
|
||||
# The load-bearing assertion of P4: a successful auth from a
|
||||
# DIFFERENT spoofed source IP proves the lockout was scoped
|
||||
# to (admin, 10.0.0.1) and not to admin alone.
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
X-Forwarded-For: 10.0.0.2
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
|
||||
HTTP 207
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Teardown — Revoke the app password we minted. Keeps the
|
||||
# app-passwords table clean across re-runs.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DELETE {{base_url}}/api/auth/app-passwords/{{ap_id}}
|
||||
Authorization: Bearer {{jwt}}
|
||||
|
||||
HTTP 200
|
||||
Reference in New Issue
Block a user