Skip to content

Commit c920ed5

Browse files
committed
Add optional basic auth guardrails for proxy and web library
1 parent d729baa commit c920ed5

File tree

12 files changed

+199
-7
lines changed

12 files changed

+199
-7
lines changed

.env.dev.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ DEV_WEB_LIBRARY_PORT=8281
1717
# Set to a host reachable by your browser (not localhost if accessed via NAS).
1818
DEV_PDF_PROXY_BASE_URL=http://192.168.0.50:8280/pdf
1919

20+
# Optional HTTP basic auth (useful when reverse proxy auth is bypassed)
21+
DEV_PDF_PROXY_BASIC_AUTH_USER=
22+
DEV_PDF_PROXY_BASIC_AUTH_PASSWORD=
23+
DEV_PDF_PROXY_BASIC_AUTH_REALM=Zotero PDF Proxy
24+
DEV_WEB_LIBRARY_BASIC_AUTH_USER=
25+
DEV_WEB_LIBRARY_BASIC_AUTH_PASSWORD=
26+
DEV_WEB_LIBRARY_BASIC_AUTH_REALM=Zotero WebUI
27+
2028
# On-prem upload gating (set false to block WebUI uploads to Zotero Storage; sync via Desktop/WebDAV instead)
2129
WEB_LIBRARY_ALLOW_UPLOADS=false
2230

.env.portainer.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ WEB_LIBRARY_PORT=8281
1414
# Base URL that the Web Library should use when linking to the PDF proxy (update to your staging hostname/IP)
1515
PDF_PROXY_BASE_URL=http://192.168.0.50:8280/pdf
1616

17+
# Optional HTTP basic auth (extra guard if Cloudflare/NPM is bypassed)
18+
PDF_PROXY_BASIC_AUTH_USER=
19+
PDF_PROXY_BASIC_AUTH_PASSWORD=
20+
PDF_PROXY_BASIC_AUTH_REALM=Zotero PDF Proxy
21+
WEB_LIBRARY_BASIC_AUTH_USER=
22+
WEB_LIBRARY_BASIC_AUTH_PASSWORD=
23+
WEB_LIBRARY_BASIC_AUTH_REALM=Zotero WebUI
24+
1725
# Metadata source (runtime templating for the Web Library)
1826
# If you host metadata on zotero.org (default):
1927
# ZOTERO_API_KEY: your Zotero API key

.env.stage.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ WEB_LIBRARY_PORT=8281
1414
# Base URL that the Web Library should use when linking to the PDF proxy (update to your staging hostname/IP)
1515
PDF_PROXY_BASE_URL=http://192.168.0.50:8280/pdf
1616

17+
# Optional HTTP basic auth (extra guard if Cloudflare/NPM is bypassed)
18+
PDF_PROXY_BASIC_AUTH_USER=
19+
PDF_PROXY_BASIC_AUTH_PASSWORD=
20+
PDF_PROXY_BASIC_AUTH_REALM=Zotero PDF Proxy
21+
WEB_LIBRARY_BASIC_AUTH_USER=
22+
WEB_LIBRARY_BASIC_AUTH_PASSWORD=
23+
WEB_LIBRARY_BASIC_AUTH_REALM=Zotero WebUI
24+
1725
# Metadata source (runtime templating for the Web Library)
1826
# If you host metadata on zotero.org (default):
1927
# ZOTERO_API_KEY: your Zotero API key

Dockerfile.web-library

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ RUN npm ci \
3232
FROM nginx:1.27-alpine AS runtime
3333

3434
# For envsubst and clean startup
35-
RUN apk add --no-cache gettext
35+
RUN apk add --no-cache apache2-utils gettext
3636

3737
COPY --from=builder /app/web-library-build/build/ /usr/share/nginx/html/
38-
COPY app/web-library-overlay/config/nginx.conf /etc/nginx/conf.d/default.conf
38+
COPY app/web-library-overlay/config/nginx.conf.template /etc/nginx/conf.d/default.conf.template
3939
COPY app/web-library-overlay/scripts/entrypoint.sh /entrypoint.sh
4040
RUN chmod +x /entrypoint.sh
4141

app/main.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import logging
22
import os
33
import re
4+
import secrets
45
import zipfile
56
from pathlib import Path
67
from typing import Iterable, Optional
78

8-
from fastapi import FastAPI, HTTPException
9+
from fastapi import Depends, FastAPI, HTTPException
910
from fastapi.responses import StreamingResponse
11+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
1012

1113

1214
logging.basicConfig(level=logging.INFO)
@@ -22,6 +24,11 @@ def get_zotero_root() -> Path:
2224

2325

2426
app = FastAPI(title="Zotero WebDAV PDF Proxy")
27+
basic_auth_scheme = HTTPBasic(auto_error=False)
28+
29+
BASIC_AUTH_USER = os.getenv("PDF_PROXY_BASIC_AUTH_USER")
30+
BASIC_AUTH_PASSWORD = os.getenv("PDF_PROXY_BASIC_AUTH_PASSWORD")
31+
BASIC_AUTH_REALM = os.getenv("PDF_PROXY_BASIC_AUTH_REALM", "Zotero PDF Proxy")
2532

2633

2734
def validate_key(key: str) -> None:
@@ -103,12 +110,35 @@ def iterator() -> Iterable[bytes]:
103110
return StreamingResponse(iterator(), media_type="application/pdf", headers=headers)
104111

105112

106-
@app.get("/health")
113+
def enforce_basic_auth(
114+
credentials: Optional[HTTPBasicCredentials] = Depends(basic_auth_scheme),
115+
) -> None:
116+
auth_configured = BASIC_AUTH_USER and BASIC_AUTH_PASSWORD
117+
if not auth_configured:
118+
return
119+
120+
unauthorized = HTTPException(
121+
status_code=401,
122+
detail="Unauthorized",
123+
headers={"WWW-Authenticate": f'Basic realm="{BASIC_AUTH_REALM}"'},
124+
)
125+
126+
if credentials is None:
127+
raise unauthorized
128+
129+
username_valid = secrets.compare_digest(credentials.username, BASIC_AUTH_USER)
130+
password_valid = secrets.compare_digest(credentials.password, BASIC_AUTH_PASSWORD)
131+
132+
if not username_valid or not password_valid:
133+
raise unauthorized
134+
135+
136+
@app.get("/health", dependencies=[Depends(enforce_basic_auth)])
107137
def health() -> dict:
108138
return {"status": "ok"}
109139

110140

111-
@app.get("/pdf/{key}")
141+
@app.get("/pdf/{key}", dependencies=[Depends(enforce_basic_auth)])
112142
def get_pdf(key: str):
113143
validate_key(key)
114144

@@ -128,4 +158,3 @@ def get_pdf(key: str):
128158
except Exception as error:
129159
logger.exception("Unexpected error while processing key=%s", key)
130160
raise HTTPException(status_code=500, detail="Internal server error") from error
131-

app/web-library-overlay/config/nginx.conf renamed to app/web-library-overlay/config/nginx.conf.template

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ server {
55
root /usr/share/nginx/html;
66
index index.html;
77

8+
${AUTH_SNIPPET}
9+
810
location / {
911
try_files $uri $uri/ /index.html;
1012
}

app/web-library-overlay/scripts/entrypoint.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ set -e
1111
: "${ZOTERO_LIBRARIES_INCLUDE_JSON:=[]}"
1212
: "${WEB_LIBRARY_ALLOW_UPLOADS:=false}"
1313
: "${PDF_PROXY_BASE_URL:=http://localhost:8280/pdf}"
14+
: "${WEB_LIBRARY_BASIC_AUTH_REALM:=Zotero WebUI}"
1415

1516
# Render index.html with current env vars (placeholders remain if empty)
1617
if [ -f /usr/share/nginx/html/index.html ]; then
@@ -21,4 +22,18 @@ if [ -f /usr/share/nginx/html/index.html ]; then
2122
mv /usr/share/nginx/html/index.html.rendered /usr/share/nginx/html/index.html
2223
fi
2324

25+
AUTH_SNIPPET=""
26+
27+
if [ -n "${WEB_LIBRARY_BASIC_AUTH_USER:-}" ] && [ -n "${WEB_LIBRARY_BASIC_AUTH_PASSWORD:-}" ]; then
28+
HTPASSWD_PATH="/etc/nginx/.htpasswd"
29+
htpasswd -bBc "$HTPASSWD_PATH" "$WEB_LIBRARY_BASIC_AUTH_USER" "$WEB_LIBRARY_BASIC_AUTH_PASSWORD"
30+
AUTH_SNIPPET=$(printf " auth_basic \"%s\";\n auth_basic_user_file %s;" "$WEB_LIBRARY_BASIC_AUTH_REALM" "$HTPASSWD_PATH")
31+
fi
32+
33+
if [ -f /etc/nginx/conf.d/default.conf.template ]; then
34+
AUTH_SNIPPET="$AUTH_SNIPPET" envsubst '$AUTH_SNIPPET' \
35+
< /etc/nginx/conf.d/default.conf.template \
36+
> /etc/nginx/conf.d/default.conf
37+
fi
38+
2439
exec "$@"

docker-compose.dev.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ services:
88
dockerfile: Dockerfile.pdf-proxy
99
environment:
1010
- ZOTERO_ROOT=/data/zotero
11+
- PDF_PROXY_BASIC_AUTH_USER=${DEV_PDF_PROXY_BASIC_AUTH_USER:-}
12+
- PDF_PROXY_BASIC_AUTH_PASSWORD=${DEV_PDF_PROXY_BASIC_AUTH_PASSWORD:-}
13+
- PDF_PROXY_BASIC_AUTH_REALM=${DEV_PDF_PROXY_BASIC_AUTH_REALM:-Zotero PDF Proxy}
1114
volumes:
1215
- ${DEV_ZOTERO_SAMPLE_ROOT:-./sample-webdav/zotero}:/data/zotero
1316
- ./app:/app
@@ -29,6 +32,9 @@ services:
2932
- ZOTERO_INCLUDE_USER_GROUPS=${ZOTERO_INCLUDE_USER_GROUPS:-true}
3033
- ZOTERO_LIBRARIES_INCLUDE_JSON=${ZOTERO_LIBRARIES_INCLUDE_JSON:-[]}
3134
- WEB_LIBRARY_ALLOW_UPLOADS=${WEB_LIBRARY_ALLOW_UPLOADS:-false}
35+
- WEB_LIBRARY_BASIC_AUTH_USER=${DEV_WEB_LIBRARY_BASIC_AUTH_USER:-}
36+
- WEB_LIBRARY_BASIC_AUTH_PASSWORD=${DEV_WEB_LIBRARY_BASIC_AUTH_PASSWORD:-}
37+
- WEB_LIBRARY_BASIC_AUTH_REALM=${DEV_WEB_LIBRARY_BASIC_AUTH_REALM:-Zotero WebUI}
3238
ports:
3339
- "${DEV_WEB_LIBRARY_PORT:-8281}:80"
3440
depends_on:

docker-compose.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ services:
55
image: ${PDF_PROXY_IMAGE:-ghcr.io/joonsoome/on-prem-zotero-webui/pdf-proxy:main}
66
environment:
77
- ZOTERO_ROOT=/data/zotero
8+
- PDF_PROXY_BASIC_AUTH_USER=${PDF_PROXY_BASIC_AUTH_USER:-}
9+
- PDF_PROXY_BASIC_AUTH_PASSWORD=${PDF_PROXY_BASIC_AUTH_PASSWORD:-}
10+
- PDF_PROXY_BASIC_AUTH_REALM=${PDF_PROXY_BASIC_AUTH_REALM:-Zotero PDF Proxy}
811
volumes:
912
- ${ZOTERO_ROOT_HOST_PATH:-/volume1/Reference/zotero}:/data/zotero
1013
ports:
@@ -23,6 +26,9 @@ services:
2326
- ZOTERO_INCLUDE_USER_GROUPS=${ZOTERO_INCLUDE_USER_GROUPS:-true}
2427
- ZOTERO_LIBRARIES_INCLUDE_JSON=${ZOTERO_LIBRARIES_INCLUDE_JSON:-[]}
2528
- WEB_LIBRARY_ALLOW_UPLOADS=${WEB_LIBRARY_ALLOW_UPLOADS:-false}
29+
- WEB_LIBRARY_BASIC_AUTH_USER=${WEB_LIBRARY_BASIC_AUTH_USER:-}
30+
- WEB_LIBRARY_BASIC_AUTH_PASSWORD=${WEB_LIBRARY_BASIC_AUTH_PASSWORD:-}
31+
- WEB_LIBRARY_BASIC_AUTH_REALM=${WEB_LIBRARY_BASIC_AUTH_REALM:-Zotero WebUI}
2632
ports:
2733
- "${WEB_LIBRARY_PORT:-8281}:80"
2834
depends_on:

requirements-dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
-r app/requirements.txt
22
pytest
3-
3+
httpx

0 commit comments

Comments
 (0)