Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
064fd90
docs: add Safe Expunging Process policy
level09 Apr 21, 2026
14355e5
fix(docker): switch nginx base to bitnamilegacy/nginx
level09 Apr 21, 2026
3c754fd
fix(docker): install runtime deps for celery-ocr role
level09 Apr 21, 2026
2f9e395
fix(docker): default ENV_FILE to .env.docker
level09 Apr 21, 2026
3aeaff1
fix(docker): exclude .git, node_modules, caches from build context
level09 Apr 21, 2026
cd51dac
fix(docker): stamp head on fresh DB instead of running migrations
level09 Apr 21, 2026
b136add
refactor(docker): slim-bookworm base with dedicated uv builder stage
level09 Apr 21, 2026
f6cd5f4
fix(docker): use TCP probe for nginx healthcheck
level09 Apr 21, 2026
244ee1d
v4.0.1: fix bulk OCR celery queue routing (#323)
level09 Apr 23, 2026
80f56b9
fix(BAY-01-001): re-check object access in revision history endpoints
level09 May 1, 2026
d0f4581
fix(BAY-01-002): enforce object-level access on extraction PUT
level09 May 1, 2026
4b66981
fix(BAY-01-004): contain CSV/XLS analyze paths inside IMPORT_DIR
level09 May 1, 2026
6b69734
fix(BAY-01-007): rate-limit failed logins by username and IP
level09 May 1, 2026
9800fbe
fix(BAY-01-008): sanitize imported rich-text fields
level09 May 1, 2026
3312be7
merge: bring v4.0.1 OCR queue routing fix from main
level09 May 1, 2026
903b19e
test(BAY-01): regression tests for Wave 1 pentest fixes
level09 May 1, 2026
ecc4aa7
refactor(BAY-01-007): use Flask-Limiter and move limits to settings
level09 May 1, 2026
507bab0
fix(BAY-01-006): drop DB superuser, replace trust auth with peer
level09 May 1, 2026
3ce769f
fix(BAY-01-003): filter export items by requester.can_access
level09 May 1, 2026
597e7e6
fix(BAY-01-005): drop /api/create-admin, bootstrap admin via installe…
level09 May 1, 2026
adb4139
polish(BAY-01-005): clearer install banner with login URL
level09 May 1, 2026
dde75cc
fix(BAY-01-005): bootstrap admin in Docker entrypoint on fresh DB
level09 May 1, 2026
ed81474
docs(BAY-01-005): correct Docker admin retrieval — service is bayanat
level09 May 1, 2026
1d80020
fix(BAY-01-005): pass admin password via stdin, not argv
level09 May 1, 2026
8996e2d
fix(settings): URL-encode DB and Redis passwords in connection URLs
level09 May 3, 2026
c96974b
docs(BAY-01-005): admin bootstrap, Compose v2, env-file
level09 May 7, 2026
726d345
fix(BAY-01-041): cap media import Celery task with soft/hard time limit
level09 May 14, 2026
6f562d4
fix(BAY-01-038): coerce parse_excel column labels to strings
level09 May 14, 2026
5abe376
fix(BAY-01-036): pin Excel engine to openpyxl
level09 May 14, 2026
f199080
fix(BAY-01-035): keep parse_csv robust to ragged rows
level09 May 16, 2026
c0adefa
fix(BAY-01-043): drop URL-derived Source fallback in web import
level09 May 16, 2026
9bfca8a
fix(BAY-01-040): type-guard malformed bodies in Export.from_json
level09 May 17, 2026
0614f8a
fix(BAY-01-009): enforce edit boundary on media update endpoint
level09 May 18, 2026
b186e1b
fix(BAY-01-042): type-guard malformed bodies in import API
level09 May 19, 2026
dec0516
fix(BAY-01-011): strip rolesReplace for non-Admin in bulk update
level09 May 19, 2026
8b0c2d5
fix(BAY-01-012): enforce can_access_media on direct media endpoints
level09 May 19, 2026
9ee85af
fix(BAY-01-037): type-guard sheet parameter in XLSX analyze
level09 May 20, 2026
f34e679
fix(BAY-01-010): centralize per-target access check in relation sync
level09 May 20, 2026
aa7ecb0
fix(BAY-01-039): sanitize handle_mismatch description sink
level09 May 22, 2026
e4f26ca
fix(BAY-01-013): remove web/celery update bridge, web update is read-…
level09 May 24, 2026
a9af9db
chore: gitignore release signing keys
level09 May 24, 2026
09f3f60
fix(BAY-01-024): neutralize CSV formula injection in exports
level09 May 24, 2026
ddabe8e
fix(BAY-01-014): match registered domain in web-import allowlist
level09 May 24, 2026
4f8b40e
fix(BAY-01-015): enforce ownership on export detail endpoint
level09 May 24, 2026
ee18f11
fix(BAY-01-019): enforce Google subject binding on OAuth login
level09 May 24, 2026
ce2afeb
fix(BAY-01-018): scope bulk revisions/activity to accessible items
level09 May 24, 2026
f541199
fix(BAY-01-021): mask assignee/reviewer names in item list APIs
level09 May 24, 2026
1805951
fix(BAY-01-016): enforce config-driven session freshness on privilege…
level09 May 24, 2026
76f431f
fix(BAY-01-026): scope export items to requester access at creation
level09 May 25, 2026
0cba246
fix(BAY-01-023): cap PDF rasterization at the OCR page limit
level09 May 25, 2026
4708fe4
fix(BAY-01-025): block external/file resource fetching in PDF export
level09 May 25, 2026
f55f7a0
fix(BAY-01-020): opaque filenames for inline media uploads
level09 May 25, 2026
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
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
.git
node_modules
.venv
env
__pycache__
*.pyc
*.pyo
enferno/media
enferno/imports
logs
backups
.env
.env.*
.DS_Store
*.md
docs/node_modules
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ backups/*
cookies.txt

*.egg-info/

# Release signing: NEVER commit secret keys. The pinned public key is baked
# into the installer/updater, not stored as a loose file in the repo.
*.key
bayanat-release.key
bayanat-release.pub
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## v4.0.1

### Fixed

- Bulk OCR: celery worker now consumes the `ocr` queue. The systemd unit written by the installer was only subscribing to the default `celery` queue, so tasks dispatched by bulk OCR (UI and `flask ocr process`) silently piled up in Redis. Single-media OCR was not affected. Existing installs can fix in place by adding `-Q celery,ocr` to `ExecStart` in `/etc/systemd/system/bayanat-celery.service`, then `systemctl daemon-reload && systemctl restart bayanat-celery`.

## v4.0.0

### Database Migrations (Alembic)
Expand Down
43 changes: 43 additions & 0 deletions SAFE_EXPUNGING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Safe Expunging Process

This document describes when and how history-altering operations are permitted on the Bayanat source repository, satisfying the SLSA v1.2 Source track "Safe Expunging Process" requirement.

## Scope

Applies to the public repository `sjacorg/bayanat` and the private release repository `sjacorg/bayanat.prod`, specifically to operations that remove or rewrite committed history on protected references (`main`, release tags matching `v*`).

## Default

History on protected references is append-only. Force-push, branch deletion, tag deletion, and retagging are blocked by repository rulesets.

## Permitted Reasons to Expunge

Expunging may be approved only for one of the following reasons:

1. **Secret leak.** An unredacted credential, private key, or access token was committed.
2. **Personal data leak.** Non-public personal data of an identifiable individual was committed.
3. **Legal or safety order.** A verified order from counsel or a credible safety concern requires removal of specific content.
4. **Malicious injection.** Attacker-introduced code or data must be removed as part of incident response.

Bug fixes, style corrections, and cleanup are never valid reasons.

## Approval

Both maintainers must approve in writing, recorded in the security advisory created for the incident.

## Procedure

1. File a private security advisory at https://github.com/sjacorg/bayanat/security/advisories with the reason, affected commits, and proposed action.
2. Record both maintainer approvals in the advisory.
3. If the reason involves a secret, rotate it before rewriting.
4. Rewrite with `git filter-repo` (not `filter-branch`), preserving commit signatures where possible.
5. Temporarily bypass branch protection, force-update the protected reference, then re-enable protection.
6. Invalidate and regenerate any affected release tags. Old tags are not reused.

## Consumer Notification

After any expunging action, publish a public security advisory that includes:

- What was removed and why (redacted as needed).
- New commit hashes and release tags that replace the expunged revisions.
- Operator guidance (re-clone, re-verify signatures, check deployed commit against the new history).
97 changes: 71 additions & 26 deletions bayanat
Original file line number Diff line number Diff line change
Expand Up @@ -608,19 +608,31 @@ _setup_database() {
systemctl enable --now postgresql redis-server

log "Setting up database..."
sudo -u postgres createuser -s "$APP_USER" 2>/dev/null || true
# Role is a plain owner, not a superuser. Extensions are created by the
# postgres superuser below so the app role doesn't need that privilege.
sudo -u postgres createuser "$APP_USER" 2>/dev/null || true
# Idempotent for upgrades: drop superuser if a previous install granted it.
sudo -u postgres psql -c "ALTER USER \"$APP_USER\" NOSUPERUSER;" 2>/dev/null || true
sudo -u postgres createdb bayanat -O "$APP_USER" 2>/dev/null || true

# Configure pg_hba trust auth for app user
sudo -u postgres psql -d bayanat \
-c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" \
-c "CREATE EXTENSION IF NOT EXISTS postgis;" >/dev/null

# Configure pg_hba peer auth for app user. Peer auth maps the OS user to
# the PG role over the local socket, so only processes running as
# $APP_USER can connect as $APP_USER. Replaces the previous 'trust' rule
# which let any local OS user connect as the app role.
local pg_hba
pg_hba=$(find /etc/postgresql -name pg_hba.conf 2>/dev/null | head -1)
[[ -n "$pg_hba" ]] || die "Cannot find pg_hba.conf"

local rule="local all $APP_USER trust"
local rule="local all $APP_USER peer"
if grep -qF "$rule" "$pg_hba"; then
log "pg_hba.conf already configured"
else
log "Configuring pg_hba.conf for $APP_USER"
# Remove any prior trust rule for this user from a previous install
sed -i "/^local[[:space:]]\+all[[:space:]]\+$APP_USER[[:space:]]\+.*trust/d" "$pg_hba"
# Insert before the "local all all" catch-all, or append
if grep -q "^local.*all.*all" "$pg_hba"; then
sed -i "/^local.*all.*all/i $rule" "$pg_hba"
Expand Down Expand Up @@ -724,6 +736,36 @@ _init_database() {
fi
}

ADMIN_USERNAME=""
ADMIN_PASSWORD=""

_bootstrap_admin() {
# Provision the initial admin out-of-band. Replaces the deleted
# /api/create-admin wizard endpoint, which was unauthenticated and
# claimable by the first network client during the install window.
# The password is fed to flask install over stdin (--password-stdin)
# so it is never visible in /proc/<pid>/cmdline or `ps`.
local tag="$1"
local pw
pw=$(python3 -c "import secrets; print(secrets.token_urlsafe(20))" 2>/dev/null) || \
pw=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)

log "Bootstrapping initial admin user..."
local out
out=$(printf '%s\n' "$pw" \
| flask_run "$tag" install --username admin --password-stdin 2>&1) || true

if echo "$out" | grep -q "already installed"; then
log "Admin user already exists, skipping bootstrap"
elif echo "$out" | grep -q "installed successfully"; then
ADMIN_USERNAME="admin"
ADMIN_PASSWORD="$pw"
else
warn "Admin bootstrap unexpected output:"
warn "$out"
fi
}

_install_uwsgi_config() {
cat > "$SHARED_DIR/uwsgi-prod.ini" << EOF
[uwsgi]
Expand Down Expand Up @@ -776,7 +818,7 @@ User=$APP_USER
Group=$APP_USER
WorkingDirectory=$CURRENT_LINK
EnvironmentFile=$SHARED_DIR/.env
ExecStart=$CURRENT_LINK/.venv/bin/celery -A enferno.tasks worker --autoscale 2,5 -B
ExecStart=$CURRENT_LINK/.venv/bin/celery -A enferno.tasks worker --autoscale 2,5 -B -Q celery,ocr
Restart=always
RestartSec=3

Expand Down Expand Up @@ -819,7 +861,6 @@ EOF

_install_sudoers() {
cat > /etc/sudoers.d/bayanat << 'EOF'
bayanat ALL=(root) NOPASSWD: /usr/local/sbin/bayanat-start-update
bayanat ALL=(root) NOPASSWD: /usr/local/bin/bayanat status
bayanat ALL=(root) NOPASSWD: /usr/local/bin/bayanat snapshots
bayanat ALL=(root) NOPASSWD: /usr/bin/systemctl restart bayanat-celery
Expand All @@ -828,22 +869,6 @@ EOF
visudo -cf /etc/sudoers.d/bayanat >/dev/null || die "sudoers syntax invalid"
}

_install_update_wrapper() {
# Root-owned wrapper. Launches `bayanat update` as a transient systemd
# unit so the update outlives Flask restart, SSH disconnect, and
# browser close. Must be in sudoers at this exact path.
install -m 0755 -o root -g root /dev/stdin /usr/local/sbin/bayanat-start-update <<'EOF'
#!/bin/bash
# Installed root:root 0755 by `bayanat install`. Do not edit.
set -euo pipefail
exec /usr/bin/systemd-run \
--unit=bayanat-update \
--collect \
--property=Restart=no \
/usr/local/bin/bayanat update
EOF
}

_install_self() {
# Copy the bayanat CLI from the given release directory to
# /usr/local/bin/bayanat. Source is $RELEASES_DIR/$tag/bayanat, NOT $0 —
Expand Down Expand Up @@ -895,6 +920,7 @@ cmd_install() {

_install_deps "$tag"
_init_database "$tag"
_bootstrap_admin "$tag"

# Activate release
swap_symlink "$RELEASES_DIR/$tag"
Expand All @@ -905,7 +931,6 @@ cmd_install() {
_install_systemd
_configure_caddy "$domain"
_install_sudoers
_install_update_wrapper
_install_self "$tag"

chown -R "$APP_USER:$APP_USER" "$BAYANAT_ROOT"
Expand All @@ -915,11 +940,31 @@ cmd_install() {

_verify_service_health

log "Installation complete"
local access_url
if [[ "$domain" == "localhost" ]]; then
log "Access: http://$(hostname -I | awk '{print $1}')"
access_url="http://$(hostname -I | awk '{print $1}')"
else
log "Access: https://$domain"
access_url="https://$domain"
fi

log "Installation complete"
log "Access: $access_url"

if [[ -n "$ADMIN_PASSWORD" ]]; then
log ""
log "============================================================"
log " Bayanat is ready. Sign in to finish setup:"
log ""
log " URL : $access_url/login"
log " Username : $ADMIN_USERNAME"
log " Password : $ADMIN_PASSWORD"
log ""
log " Save these credentials now - the password is not stored"
log " in plaintext anywhere. After signing in, the setup wizard"
log " will walk you through language, default data, and other"
log " configuration. Change the password from your account"
log " settings."
log "============================================================"
fi
}

Expand Down
14 changes: 7 additions & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ services:
dockerfile: ./flask/Dockerfile
args:
- ROLE=flask
- ENV_FILE=${ENV_FILE:-.env}
- ENV_FILE=${ENV_FILE:-.env.docker}
volumes:
- '${PWD}/backups:/app/backups/:rw'
- '${MEDIA_PATH:-./enferno/media}:/app/enferno/media/:rw'
- '${PWD}/enferno/imports:/app/enferno/imports/:rw'
- '${PWD}/logs/:/app/logs/:rw'
- '${PWD}/config.json:/app/config.json:rw'
- '${PWD}/${ENV_FILE:-.env}:/app/.env:ro'
- '${PWD}/${ENV_FILE:-.env.docker}:/app/.env:ro'
depends_on:
postgres:
condition: service_healthy
Expand All @@ -72,7 +72,7 @@ services:
dockerfile: ./flask/Dockerfile
args:
- ROLE=celery
- ENV_FILE=${ENV_FILE:-.env}
- ENV_FILE=${ENV_FILE:-.env.docker}
volumes_from:
- bayanat
read_only: true
Expand All @@ -93,7 +93,7 @@ services:
dockerfile: ./flask/Dockerfile
args:
- ROLE=celery-ocr
- ENV_FILE=${ENV_FILE:-.env}
- ENV_FILE=${ENV_FILE:-.env.docker}
volumes_from:
- bayanat
read_only: true
Expand Down Expand Up @@ -127,9 +127,9 @@ services:
- /opt/bitnami/nginx/logs/
- /opt/bitnami/nginx/conf/bitnami/certs/
healthcheck:
test: [ "CMD", "service", "nginx", "status" ]
interval: 3s
retries: 10
test: [ "CMD", "bash", "-c", "exec 3<>/dev/tcp/localhost/80" ]
interval: 10s
retries: 5

volumes:
redis_data:
Expand Down
11 changes: 6 additions & 5 deletions docs/deployment/auto-update-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ sudo bayanat update --recover
| Path | Purpose |
|---|---|
| `/usr/local/bin/bayanat` | The CLI script |
| `/usr/local/sbin/bayanat-start-update` | Root wrapper the UI invokes via sudo |
| `/etc/sudoers.d/bayanat` | Granted commands for the `bayanat` user |
| `/opt/bayanat/state/update.json` | Current update state (sanitized JSON) |
| `/opt/bayanat/state/update.lock` | PID lock file |
Expand All @@ -106,10 +105,12 @@ sudo bayanat update --recover

## Admin UI surface

- Nav-bar banner chip: shows when `latest != current`
- Progress dialog: polls `/admin/api/updates/status` every 2 s during an
active update
- Settings toggle: System Administration -> "Auto-apply patch releases"
The UI is read-only for updates: it surfaces availability but never applies
an update. Updates run from the CLI as root (`sudo bayanat update`).

- Nav-bar banner chip: shows when `latest != current`, with the CLI command
to run on the server
- Status: `/admin/api/updates/status` reflects a CLI-initiated update's state
- Snapshots page: `/admin/snapshots/` (read-only list; restore stays on
the CLI)

Expand Down
36 changes: 29 additions & 7 deletions docs/deployment/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,53 @@ Docker Compose deployment is still in beta. For production environments, [native

## Prerequisites

- Docker and Docker Compose installed
- `.env` file configured (see [Configuration](/deployment/configuration))
- Docker Engine with the Compose v2 plugin (`docker compose`, not the legacy `docker-compose` binary)
- `.env.docker` file configured (see [Configuration](/deployment/configuration))

## Quick Start

```bash
docker-compose up -d
docker compose --env-file .env.docker up -d
```

This starts PostgreSQL, Redis, the Flask app, NGINX, and Celery.

## Create Admin User
::: tip
The `--env-file .env.docker` flag is required so Compose can substitute `${POSTGRES_USER}`, `${POSTGRES_PASSWORD}`, and `${REDIS_PASSWORD}` placeholders in `docker-compose.yml`. Without it, those services boot with empty credentials and the Flask container fails to connect.
:::

## First Admin User

The entrypoint creates an `admin` user automatically on the first startup
(when the database has no schema yet) and prints a one-time random
password to the container logs. Retrieve it with:

```bash
docker-compose exec bayanat uv run flask install
docker compose --env-file .env.docker logs bayanat | grep -A4 "Generated password"
```

Sign in at the Bayanat URL with `admin` and the printed password. The
setup wizard runs after first login. Change the admin password from your
account settings afterwards.

If the auto-bootstrap was missed or the admin account was deleted, run
the CLI directly:

```bash
docker compose --env-file .env.docker exec bayanat uv run flask install -u admin
```

It generates a fresh password and prints it. If an admin already exists
the command exits without changing anything.

## Development

```bash
docker-compose -f docker-compose-dev.yml up
docker compose -f docker-compose-dev.yml up
```

## Testing

```bash
docker-compose -f docker-compose-test.yml up
docker compose -f docker-compose-test.yml up
```
Loading
Loading