deploy_kds: scoped DB role instead of sharing kitchen's credential

- Generate KDS_DB_PASS (matching KITCHEN_DB_PASS's pattern), add to --only
  dispatch's _append_secret list
- Create a scoped `kds` Postgres role: SELECT/INSERT/UPDATE/DELETE on
  kds_tickets/kds_course_bumps, read-only on resos_bookings/resos_opening_hours,
  column-scoped SELECT+UPDATE on just the kds_* columns of kitchen_settings
  (not the NewBook/ResOS/Nextcloud/Dext/SambaPOS/Azure/Anthropic credentials
  that live in the same table). Runs in two passes since kds_tickets/
  kds_course_bumps don't exist until KDS's own migrations create them.
- kds .env now carries DATABASE_URL (scoped `kds` role, runtime) and
  MIGRATION_DATABASE_URL (privileged `kitchen` role, migrations only)
- Fix KDS theme_color seed (teal -> orange, matched kitchen's tile before)

See kitchen-port-log.md E17.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-06 14:45:07 +00:00
parent dbf9e90164
commit 2088cb5577

View file

@ -2189,6 +2189,12 @@ deploy_kds() {
msg_error "KITCHEN_DB_PASS not set — deploy kitchen first, or add it to ${CREDS_FILE}" msg_error "KITCHEN_DB_PASS not set — deploy kitchen first, or add it to ${CREDS_FILE}"
fi fi
if [[ -z "${KDS_DB_PASS:-}" ]]; then
KDS_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
printf '\nKDS_DB_PASS=%s\n' "$KDS_DB_PASS" >> "$CREDS_FILE"
msg_ok "Generated KDS_DB_PASS → ${CREDS_FILE}"
fi
msg_info "Creating LXC 125" msg_info "Creating LXC 125"
create_lxc 125 "10.10.10.125" "kds" 1024 1 create_lxc 125 "10.10.10.125" "kds" 1024 1
msg_ok "LXC 125 created" msg_ok "LXC 125 created"
@ -2198,12 +2204,54 @@ deploy_kds() {
install_mgmt_key 125 install_mgmt_key 125
msg_ok "Docker + SSH ready" msg_ok "Docker + SSH ready"
# Scoped `kds` DB role — E17: KDS used to share the `kitchen` role wholesale
# (full read/write on all 74 kitchen_db tables). Grant only what KDS
# actually touches: its own tables, read-only ResOS bookings, and the
# kds_* columns on kitchen_settings — not NewBook/ResOS/Nextcloud/Dext/
# SambaPOS-sales/Azure/Anthropic credentials that live in the same table.
# kds_tickets/kds_course_bumps don't exist yet at this point (KDS's own
# migrations create them on first boot, run via the privileged connection
# below) — that grant runs in a second pass after the container is up.
msg_info "Scoping kds DB role"
pct exec 100 -- docker exec hotel-manage-postgres psql -U postgres -d kitchen_db -c "
DO \$\$ BEGIN
CREATE ROLE kds LOGIN PASSWORD '${KDS_DB_PASS}';
EXCEPTION WHEN duplicate_object THEN
ALTER ROLE kds WITH PASSWORD '${KDS_DB_PASS}';
END \$\$;
GRANT CONNECT ON DATABASE kitchen_db TO kds;
GRANT USAGE ON SCHEMA public TO kds;
GRANT SELECT ON resos_bookings, resos_opening_hours TO kds;
GRANT SELECT (
id, kitchen_id, kds_enabled, kds_graphql_url, kds_graphql_username,
kds_graphql_password, kds_graphql_client_id, kds_poll_interval_seconds,
kds_timer_green_seconds, kds_timer_amber_seconds, kds_timer_red_seconds,
kds_course_order, kds_show_completed_for_seconds,
kds_away_timer_green_seconds, kds_away_timer_amber_seconds,
kds_away_timer_red_seconds, kds_bookings_refresh_seconds
) ON kitchen_settings TO kds;
GRANT UPDATE (
kds_enabled, kds_graphql_url, kds_graphql_username, kds_graphql_password,
kds_graphql_client_id, kds_poll_interval_seconds, kds_timer_green_seconds,
kds_timer_amber_seconds, kds_timer_red_seconds, kds_course_order,
kds_show_completed_for_seconds, kds_away_timer_green_seconds,
kds_away_timer_amber_seconds, kds_away_timer_red_seconds,
kds_bookings_refresh_seconds
) ON kitchen_settings TO kds;
" &>/dev/null \
&& msg_ok "kds DB role scoped" \
|| msg_warn "kds role setup failed — check LXC 100 postgres logs"
msg_info "Deploying kds" msg_info "Deploying kds"
deploy_service 125 "kds" "${REPO_ROOT}/kds" /opt/kds deploy_service 125 "kds" "${REPO_ROOT}/kds" /opt/kds
# DATABASE_URL is the scoped `kds` role (runtime queries). MIGRATION_DATABASE_URL
# is the privileged `kitchen` role, used only at startup to create KDS's own
# tables and ALTER kitchen_settings — never touched by request handling.
push_file 125 /opt/kds/.env <<EOF push_file 125 /opt/kds/.env <<EOF
APP_SLUG=kds APP_SLUG=kds
DATABASE_URL=postgresql://kitchen:${KITCHEN_DB_PASS}@10.10.10.100:5432/kitchen_db DATABASE_URL=postgresql://kds:${KDS_DB_PASS}@10.10.10.100:5432/kitchen_db
MIGRATION_DATABASE_URL=postgresql://kitchen:${KITCHEN_DB_PASS}@10.10.10.100:5432/kitchen_db
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET} CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
VITE_HOTEL_NAME=${SITE_NAME:-Hotel} VITE_HOTEL_NAME=${SITE_NAME:-Hotel}
EOF EOF
@ -2214,6 +2262,15 @@ EOF
${build_out}" ${build_out}"
fi fi
# Second grants pass — kds_tickets/kds_course_bumps now exist (created by
# KDS's own migrations on the boot above, via the privileged connection).
msg_info "Granting kds role access to its own tables"
pct exec 100 -- docker exec hotel-manage-postgres psql -U postgres -d kitchen_db -c "
GRANT SELECT, INSERT, UPDATE, DELETE ON kds_tickets, kds_course_bumps TO kds;
" &>/dev/null \
&& msg_ok "kds table grants applied" \
|| msg_warn "kds table grants failed — check kds_tickets/kds_course_bumps exist, then re-run"
msg_info "Waiting for kds" msg_info "Waiting for kds"
wait_healthy 125 "http://localhost:3080/kds/health" 200 \ wait_healthy 125 "http://localhost:3080/kds/health" 200 \
&& msg_ok "KDS running at 10.10.10.125:3080" \ && msg_ok "KDS running at 10.10.10.125:3080" \
@ -2222,7 +2279,7 @@ ${build_out}"
msg_info "Seeding kds into auth DB" msg_info "Seeding kds into auth DB"
pct exec 100 -- docker exec hotel-manage-postgres psql -U postgres -d auth_db -c " pct exec 100 -- docker exec hotel-manage-postgres psql -U postgres -d auth_db -c "
INSERT INTO apps (slug, name, description, base_path, icon, theme_color, category, internal_host, internal_port) INSERT INTO apps (slug, name, description, base_path, icon, theme_color, category, internal_host, internal_port)
VALUES ('kds', 'Kitchen Display', 'SambaPOS ticket feed and course flow display', '/kds', 'Monitor', '#0d9488', 'Kitchen', '10.10.10.125', 3080) VALUES ('kds', 'Kitchen Display', 'SambaPOS ticket feed and course flow display', '/kds', 'Monitor', '#ea580c', 'Kitchen', '10.10.10.125', 3080)
ON CONFLICT (slug) DO UPDATE SET ON CONFLICT (slug) DO UPDATE SET
name=EXCLUDED.name, description=EXCLUDED.description, base_path=EXCLUDED.base_path, name=EXCLUDED.name, description=EXCLUDED.description, base_path=EXCLUDED.base_path,
icon=EXCLUDED.icon, theme_color=EXCLUDED.theme_color, category=EXCLUDED.category, icon=EXCLUDED.icon, theme_color=EXCLUDED.theme_color, category=EXCLUDED.category,
@ -2529,6 +2586,8 @@ if [[ "${1:-}" == "--only" ]]; then
"$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)" "$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)"
[[ -z "${KITCHEN_DB_PASS:-}" ]] && _append_secret KITCHEN_DB_PASS \ [[ -z "${KITCHEN_DB_PASS:-}" ]] && _append_secret KITCHEN_DB_PASS \
"$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)" "$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)"
[[ -z "${KDS_DB_PASS:-}" ]] && _append_secret KDS_DB_PASS \
"$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)"
[[ -z "${WAGES_DB_PASS:-}" ]] && _append_secret WAGES_DB_PASS \ [[ -z "${WAGES_DB_PASS:-}" ]] && _append_secret WAGES_DB_PASS \
"$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)" "$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)"
[[ -z "${UTILITIES_DB_PASS:-}" ]] && _append_secret UTILITIES_DB_PASS \ [[ -z "${UTILITIES_DB_PASS:-}" ]] && _append_secret UTILITIES_DB_PASS \