diff --git a/src/calibre-web/README.md b/src/calibre-web/README.md index 0eb16e6..c71f0e0 100644 --- a/src/calibre-web/README.md +++ b/src/calibre-web/README.md @@ -73,12 +73,26 @@ this way. | Path | Once past the Traefik credential | Verified upstream behavior | | --- | --- | --- | -| `/opds` | Optional HTTP Basic against a real Calibre-Web account — send it for that user's own library view/permissions, omit it for the Guest role's | Basic auth is optional (not required) once anonymous browsing is on — `requires_basic_auth_if_no_ano` | +| `/opds` | The **Guest** role — Hola strips the credential it verified, so the request reaches Calibre-Web anonymous and anonymous browsing serves it | Basic auth is optional (not required) once anonymous browsing is on — `requires_basic_auth_if_no_ano` | | `/kobo//` | Per-user secret sync token in the URL path, checked independently of anonymous browsing | `401` for an invalid token, regardless of the anonymous-browsing setting | So the Traefik credential is the thing standing between these paths and the open -internet; Calibre-Web's own per-user auth underneath it is unaffected and still -personalizes access for anyone who supplies their own account credentials. +internet, and behind it `/opds` is a single shared view. + +**Why `/opds` can't be per-user.** A request carries one `Authorization` header, and +on this path it belongs to Hola's gate — so there is no room for a second, personal +Calibre-Web credential underneath. Hola strips the header after verifying it +(try-hola/hola#455); before that fix Calibre-Web received the platform credential, +looked for a user named `hola`, found none and answered `401` to every reader that had +just passed the gate. Kobo is unaffected because its per-user token rides in the URL +path, not in a header. + +That makes the Guest role the identity every OPDS reader gets, which is why the +first-run seeder grants Guest `ROLE_DOWNLOAD` and `ROLE_VIEWER` on top of +`ROLE_ANONYMOUS`. Without them Guest can list the library but every book download +answers `401` — an OPDS feed you cannot download from. The grant only fires while the +role is still Calibre-Web's untouched default, so if you edit Guest's permissions in +the UI your choice stands. One caveat worth knowing: every Authentik user who can reach the app still gets whatever Calibre-Web account they log into (or the shared Guest role, with anonymous diff --git a/src/calibre-web/package.json b/src/calibre-web/package.json index 4f5a26d..3f3ca5a 100644 --- a/src/calibre-web/package.json +++ b/src/calibre-web/package.json @@ -1,6 +1,6 @@ { "name": "calibre-web", - "version": "1.2.0", + "version": "1.2.1", "description": "Calibre-Web — browse, read, and download your Calibre ebook library (Hola app package)", "license": "GPL-3.0", "oci": { diff --git a/src/calibre-web/src/compose.yaml b/src/calibre-web/src/compose.yaml index 8838dad..acecdc1 100644 --- a/src/calibre-web/src/compose.yaml +++ b/src/calibre-web/src/compose.yaml @@ -72,8 +72,10 @@ services: # init creates it — so this can't be done from here. Instead drop a # script in LinuxServer's custom-init hook: s6 runs it after # init-calibre-web-config (which creates app.db) and before the app - # starts. It's idempotent — it only writes when the library path is - # still unset, so anything changed later in the UI is never clobbered. + # starts. Both steps below are guarded so anything changed later in the + # UI is never clobbered: the settings write only fires while the library + # path is still unset, and the Guest grant only while the role is still + # exactly Calibre-Web's untouched default. cat > /custom-cont-init.d/10-hola-first-run <<'INIT' #!/usr/bin/with-contenv bash python3 -c " @@ -97,6 +99,40 @@ services: c.commit() print('[hola] first-run defaults written to app.db') " + + # Anonymous browsing (above) makes every reader the Guest user, and + # Calibre-Web creates Guest with ROLE_ANONYMOUS (32) and nothing else — + # no ROLE_DOWNLOAD (2), no ROLE_VIEWER (256). Guest can then LIST the + # library over OPDS but every book download answers 401, which is the + # whole point of an OPDS feed. Hola's Traefik credential already gates + # /opds and /kobo/ (and it is stripped before it reaches here, so a + # reader cannot present a Calibre-Web account of its own on that path), + # so the callers that get this far are exactly the ones the operator + # authorized. Grant Guest the two read-only rights that make the app do + # what it says: download a book, and read one in the browser. Runs on + # every boot but only while the role is still the untouched default, so + # an operator who edits Guest in the UI keeps their choice. + python3 -c " + import sqlite3, os + + db = '/config/app.db' + if not os.path.exists(db): + raise SystemExit(0) + + ROLE_DOWNLOAD, ROLE_ANONYMOUS, ROLE_VIEWER = 2, 32, 256 + + c = sqlite3.connect(db) + row = c.execute('select role from user where name = ?', ('Guest',)).fetchone() + if not row or row[0] != ROLE_ANONYMOUS: + raise SystemExit(0) + + c.execute( + 'update user set role = ? where name = ?', + (ROLE_ANONYMOUS | ROLE_DOWNLOAD | ROLE_VIEWER, 'Guest'), + ) + c.commit() + print('[hola] granted Guest download + viewer rights') + " INIT chmod +x /custom-cont-init.d/10-hola-first-run diff --git a/src/calibre-web/src/manifest.json b/src/calibre-web/src/manifest.json index 16cb18a..f9d2c12 100644 --- a/src/calibre-web/src/manifest.json +++ b/src/calibre-web/src/manifest.json @@ -1,6 +1,6 @@ { "name": "calibre-web", - "version": "1.2.0", + "version": "1.2.1", "title": "Calibre-Web", "description": "Browse, read, and download your Calibre ebook library", "icon": "https://raw.githubusercontent.com/try-hola/apps/main/icons/calibre-web.svg",