Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/calibre-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<token>/` | 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
Expand Down
2 changes: 1 addition & 1 deletion src/calibre-web/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
40 changes: 38 additions & 2 deletions src/calibre-web/src/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/calibre-web/src/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading