688 lines
21 KiB
Bash
688 lines
21 KiB
Bash
#!/usr/bin/env bash
|
|
# Shared functions for the google-auth dokku plugin.
|
|
set -eo pipefail
|
|
[[ $DOKKU_TRACE ]] && set -x
|
|
|
|
GOOGLE_AUTH_PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
export DOKKU_ROOT=${DOKKU_ROOT:-/home/dokku}
|
|
export DOKKU_LIB_ROOT=${DOKKU_LIB_ROOT:-/var/lib/dokku}
|
|
export PLUGIN_CORE_AVAILABLE_PATH=${PLUGIN_CORE_AVAILABLE_PATH:-$DOKKU_LIB_ROOT/core-plugins/available}
|
|
|
|
if [[ -f "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" ]]; then
|
|
# shellcheck disable=SC1091
|
|
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
|
|
fi
|
|
|
|
GOOGLE_AUTH_DATA_ROOT="$DOKKU_LIB_ROOT/data/google-auth"
|
|
GOOGLE_AUTH_SERVICE_NAME="dokku-google-auth"
|
|
GOOGLE_AUTH_IMAGE="dokku-google-auth:latest"
|
|
GOOGLE_AUTH_DEFAULT_PORT="2999"
|
|
GOOGLE_AUTH_ROUTE_PREFIX="/_google-auth"
|
|
# Where the per-app data directory is bind-mounted inside the service container.
|
|
GOOGLE_AUTH_APP_CONFIG_MOUNT="/data/apps"
|
|
|
|
# Fallbacks so the plugin can be exercised outside a dokku host (tests, dev).
|
|
if ! declare -f dokku_log_info1 >/dev/null 2>&1; then
|
|
dokku_log_info1() { echo "-----> $*"; }
|
|
dokku_log_info2() { echo "=====> $*"; }
|
|
dokku_log_verbose() { echo " $*"; }
|
|
dokku_log_warn() { echo " ! $*" 1>&2; }
|
|
dokku_log_fail() {
|
|
echo " ! $*" 1>&2
|
|
exit 1
|
|
}
|
|
verify_app_name() {
|
|
[[ -n "$1" && -d "$DOKKU_ROOT/$1" ]] || dokku_log_fail "App $1 does not exist"
|
|
}
|
|
fi
|
|
|
|
# --- key/value storage (one file per key under the plugin data dir) ---
|
|
|
|
fn-ga-global-get() {
|
|
declare KEY="$1" DEFAULT="${2:-}"
|
|
local file="$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
|
|
if [[ -s "$file" ]]; then
|
|
head -n1 "$file"
|
|
else
|
|
printf '%s' "$DEFAULT"
|
|
fi
|
|
}
|
|
|
|
fn-ga-global-set() {
|
|
declare KEY="$1" VALUE="$2"
|
|
mkdir -p "$GOOGLE_AUTH_DATA_ROOT/global"
|
|
printf '%s\n' "$VALUE" >"$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
|
|
chmod 600 "$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
|
|
}
|
|
|
|
# Multi-value keys store one entry per line.
|
|
fn-ga-global-get-list() {
|
|
declare KEY="$1"
|
|
cat "$GOOGLE_AUTH_DATA_ROOT/global/$KEY" 2>/dev/null || true
|
|
}
|
|
|
|
fn-ga-global-set-list() {
|
|
declare KEY="$1"
|
|
shift
|
|
mkdir -p "$GOOGLE_AUTH_DATA_ROOT/global"
|
|
local file="$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
|
|
: >"$file"
|
|
local entry
|
|
for entry in "$@"; do
|
|
printf '%s\n' "$entry" >>"$file"
|
|
done
|
|
chmod 600 "$file"
|
|
}
|
|
|
|
# --- access lists, scoped to "global" or to one app ---
|
|
#
|
|
# An app's allow entries replace the global ones for that app; deny entries from
|
|
# both scopes are combined. The auth service implements that precedence — these
|
|
# helpers only store the entries.
|
|
|
|
fn-ga-list-file() {
|
|
declare SCOPE="$1" KEY="$2"
|
|
if [[ "$SCOPE" == "global" ]]; then
|
|
echo "$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
|
|
else
|
|
echo "$(fn-ga-app-dir "$SCOPE")/$KEY"
|
|
fi
|
|
}
|
|
|
|
fn-ga-list-get() {
|
|
declare SCOPE="$1" KEY="$2"
|
|
cat "$(fn-ga-list-file "$SCOPE" "$KEY")" 2>/dev/null || true
|
|
}
|
|
|
|
fn-ga-list-count() {
|
|
declare SCOPE="$1" KEY="$2"
|
|
fn-ga-list-get "$SCOPE" "$KEY" | grep -c . || true
|
|
}
|
|
|
|
fn-ga-list-contains() {
|
|
declare SCOPE="$1" KEY="$2" VALUE="$3"
|
|
grep -qxF "$VALUE" "$(fn-ga-list-file "$SCOPE" "$KEY")" 2>/dev/null
|
|
}
|
|
|
|
fn-ga-list-add() {
|
|
declare SCOPE="$1" KEY="$2" VALUE="$3"
|
|
local file
|
|
file="$(fn-ga-list-file "$SCOPE" "$KEY")"
|
|
if [[ "$SCOPE" == "global" ]]; then
|
|
mkdir -p "$GOOGLE_AUTH_DATA_ROOT/global"
|
|
touch "$file"
|
|
chmod 600 "$file"
|
|
else
|
|
fn-ga-app-dir-ensure "$SCOPE" >/dev/null
|
|
touch "$file"
|
|
# Readable through the service's read-only bind mount; see fn-ga-app-dir-ensure.
|
|
chmod 644 "$file"
|
|
fi
|
|
grep -qxF "$VALUE" "$file" || printf '%s\n' "$VALUE" >>"$file"
|
|
}
|
|
|
|
fn-ga-list-remove() {
|
|
declare SCOPE="$1" KEY="$2" VALUE="$3"
|
|
local file tmp
|
|
file="$(fn-ga-list-file "$SCOPE" "$KEY")"
|
|
[[ -f "$file" ]] || return 0
|
|
tmp="$(mktemp)"
|
|
grep -vxF "$VALUE" "$file" >"$tmp" || true
|
|
cat "$tmp" >"$file"
|
|
rm -f "$tmp"
|
|
}
|
|
|
|
# Sets GA_SCOPE from a command's first argument: "global" for --global,
|
|
# otherwise a verified app name. Fails (and exits) on anything else.
|
|
fn-ga-resolve-scope() {
|
|
declare CMD="$1" ARG="${2:-}"
|
|
case "$ARG" in
|
|
--global)
|
|
GA_SCOPE=global
|
|
;;
|
|
"")
|
|
dokku_log_fail "usage: dokku $CMD <app>|--global [<entry>...]"
|
|
;;
|
|
-*)
|
|
dokku_log_fail "unknown flag '$ARG' — pass an app name or --global"
|
|
;;
|
|
*)
|
|
verify_app_name "$ARG"
|
|
GA_SCOPE="$ARG"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Human-readable scope for log lines: "globally" or "for my-app".
|
|
fn-ga-scope-label() {
|
|
declare SCOPE="$1"
|
|
if [[ "$SCOPE" == "global" ]]; then
|
|
echo "globally"
|
|
else
|
|
echo "for $SCOPE"
|
|
fi
|
|
}
|
|
|
|
# The scope as it is typed on the command line, for suggested commands.
|
|
fn-ga-scope-arg() {
|
|
declare SCOPE="$1"
|
|
if [[ "$SCOPE" == "global" ]]; then
|
|
echo "--global"
|
|
else
|
|
echo "$SCOPE"
|
|
fi
|
|
}
|
|
|
|
# Advisory mirror of the service's allow-list precedence (an app's entries
|
|
# replace the global ones), used only to warn operators. The authority is
|
|
# emailAllowedFor in internal/authproxy.
|
|
fn-ga-email-effectively-allowed() {
|
|
declare SCOPE="$1" EMAIL="$2"
|
|
local effective="$SCOPE"
|
|
if [[ "$SCOPE" != "global" ]] &&
|
|
[[ "$(fn-ga-list-count "$SCOPE" allowed-domains)" -eq 0 &&
|
|
"$(fn-ga-list-count "$SCOPE" allowed-emails)" -eq 0 ]]; then
|
|
effective=global
|
|
fi
|
|
fn-ga-list-contains "$effective" allowed-emails "$EMAIL" && return 0
|
|
fn-ga-list-contains "$effective" allowed-domains "${EMAIL##*@}"
|
|
}
|
|
|
|
# Makes a list change take effect. Global lists travel in the container's
|
|
# environment and need a restart; per-app lists are read live through the bind
|
|
# mount, so they only need the mount to actually be there.
|
|
fn-ga-apply-list-change() {
|
|
declare SCOPE="$1"
|
|
if [[ "$SCOPE" == "global" ]]; then
|
|
fn-ga-reload-service-config
|
|
return 0
|
|
fi
|
|
# An app's lists only apply if its nginx config tells the service which app a
|
|
# request belongs to. Configs written before that header existed would make
|
|
# the app fall back to the global lists, so refresh it here rather than wait
|
|
# for the next deploy. This is a no-op when the config is already current.
|
|
if fn-google-auth-app-enabled "$SCOPE"; then
|
|
fn-ga-apply "$SCOPE"
|
|
fi
|
|
if ! fn-ga-service-running; then
|
|
dokku_log_verbose "auth service is not running; changes apply when it starts"
|
|
return 0
|
|
fi
|
|
if ! fn-ga-service-has-app-mount; then
|
|
dokku_log_info1 "recreating the auth service so it can read per-app lists"
|
|
fn-ga-service-start
|
|
return 0
|
|
fi
|
|
dokku_log_verbose "in effect within a few seconds (no restart needed)"
|
|
}
|
|
|
|
# Entries end up in a comma/space separated env var, so they may contain
|
|
# neither.
|
|
fn-ga-validate-list-entry() {
|
|
declare ENTRY="$1"
|
|
[[ -n "$ENTRY" ]] || return 1
|
|
! printf '%s' "$ENTRY" | grep -qE '[,[:space:]]'
|
|
}
|
|
|
|
# "signal.org" and "@signal.org" name a domain; "guest@partner.com" names one
|
|
# address. Callers strip any leading "@" themselves.
|
|
fn-ga-allow-entry-kind() {
|
|
declare ENTRY="$1"
|
|
if [[ "$ENTRY" == @* || "$ENTRY" != *@* ]]; then
|
|
echo domain
|
|
else
|
|
echo email
|
|
fi
|
|
}
|
|
|
|
# The service reads the allow/deny lists from its env file at startup, so
|
|
# changing them means recreating the container.
|
|
fn-ga-reload-service-config() {
|
|
if fn-ga-service-running; then
|
|
fn-ga-service-start
|
|
else
|
|
dokku_log_verbose "auth service is not running; changes apply when it starts"
|
|
fi
|
|
}
|
|
|
|
fn-ga-app-dir() {
|
|
declare APP="$1"
|
|
echo "$GOOGLE_AUTH_DATA_ROOT/apps/$APP"
|
|
}
|
|
|
|
# Creates an app's state directory with modes the auth service can use. It reads
|
|
# per-app lists through a read-only bind mount as an unprivileged uid, so the
|
|
# directories must be traversable and the list files readable. Nothing is
|
|
# exposed to other users on the host: $GOOGLE_AUTH_DATA_ROOT itself stays 0700,
|
|
# and secrets live in global/, which is never mounted.
|
|
fn-ga-app-dir-ensure() {
|
|
declare APP="$1"
|
|
local dir
|
|
dir="$(fn-ga-app-dir "$APP")"
|
|
mkdir -p "$dir"
|
|
chmod 711 "$GOOGLE_AUTH_DATA_ROOT/apps" "$dir" 2>/dev/null || true
|
|
printf '%s' "$dir"
|
|
}
|
|
|
|
fn-google-auth-app-enabled() {
|
|
declare APP="$1"
|
|
[[ -f "$(fn-ga-app-dir "$APP")/enabled" ]]
|
|
}
|
|
|
|
fn-ga-app-set-enabled() {
|
|
declare APP="$1" ENABLED="$2"
|
|
local dir
|
|
dir="$(fn-ga-app-dir "$APP")"
|
|
if [[ "$ENABLED" == "true" ]]; then
|
|
mkdir -p "$dir"
|
|
touch "$dir/enabled"
|
|
else
|
|
rm -f "$dir/enabled"
|
|
fi
|
|
}
|
|
|
|
fn-ga-excludes() {
|
|
declare APP="$1"
|
|
cat "$(fn-ga-app-dir "$APP")/excludes" 2>/dev/null || true
|
|
}
|
|
|
|
fn-ga-enabled-apps() {
|
|
local dir
|
|
for dir in "$GOOGLE_AUTH_DATA_ROOT/apps"/*/; do
|
|
[[ -d "$dir" ]] || continue
|
|
local app
|
|
app="$(basename "$dir")"
|
|
fn-google-auth-app-enabled "$app" && echo "$app"
|
|
done
|
|
return 0
|
|
}
|
|
|
|
fn-ga-configured() {
|
|
[[ -n "$(fn-ga-global-get client-id)" ]] || return 1
|
|
[[ -n "$(fn-ga-global-get client-secret)" ]] || return 1
|
|
[[ -n "$(fn-ga-global-get auth-host)" ]] || return 1
|
|
[[ -n "$(fn-ga-global-get cookie-secret)" ]] || return 1
|
|
[[ -s "$GOOGLE_AUTH_DATA_ROOT/global/allowed-domains" || -s "$GOOGLE_AUTH_DATA_ROOT/global/allowed-emails" ]] || return 1
|
|
}
|
|
|
|
# --- exclusion patterns ---
|
|
|
|
# Patterns are either a path prefix ("/api/webhooks") or a regex ("re:^/v[0-9]+/public/").
|
|
fn-ga-validate-pattern() {
|
|
declare PATTERN="$1"
|
|
[[ -n "$PATTERN" ]] || return 1
|
|
# Guard against nginx config injection.
|
|
if printf '%s' "$PATTERN" | grep -qE '[;{}"'"'"'[:space:]]'; then
|
|
return 1
|
|
fi
|
|
if [[ "$PATTERN" == re:* ]]; then
|
|
[[ -n "${PATTERN#re:}" ]] || return 1
|
|
else
|
|
[[ "$PATTERN" == /* ]] || return 1
|
|
fi
|
|
}
|
|
|
|
fn-ga-exclude-add() {
|
|
declare APP="$1" PATTERN="$2"
|
|
local dir file
|
|
dir="$(fn-ga-app-dir "$APP")"
|
|
file="$dir/excludes"
|
|
mkdir -p "$dir"
|
|
touch "$file"
|
|
grep -qxF "$PATTERN" "$file" || printf '%s\n' "$PATTERN" >>"$file"
|
|
}
|
|
|
|
fn-ga-exclude-remove() {
|
|
declare APP="$1" PATTERN="$2"
|
|
local file tmp
|
|
file="$(fn-ga-app-dir "$APP")/excludes"
|
|
[[ -f "$file" ]] || return 0
|
|
tmp="$(mktemp)"
|
|
grep -vxF "$PATTERN" "$file" >"$tmp" || true
|
|
cat "$tmp" >"$file"
|
|
rm -f "$tmp"
|
|
}
|
|
|
|
# --- nginx config generation ---
|
|
|
|
# The app's upstream block is created by dokku's own nginx template; we reuse
|
|
# it by name so excluded and protected locations proxy to the same place.
|
|
fn-ga-upstream-name() {
|
|
declare APP="$1"
|
|
local nginx_conf="$DOKKU_ROOT/$APP/nginx.conf"
|
|
[[ -f "$nginx_conf" ]] || return 1
|
|
local name
|
|
name="$(awk '$1 == "upstream" {print $2; exit}' "$nginx_conf")"
|
|
[[ -n "$name" ]] || return 1
|
|
printf '%s' "$name"
|
|
}
|
|
|
|
fn-ga-proxy-read-timeout() {
|
|
declare APP="$1"
|
|
local t="" file
|
|
for file in "$DOKKU_LIB_ROOT/config/nginx/$APP/proxy-read-timeout" \
|
|
"$DOKKU_LIB_ROOT/config/nginx/--global/proxy-read-timeout"; do
|
|
if [[ -s "$file" ]]; then
|
|
t="$(head -n1 "$file")"
|
|
break
|
|
fi
|
|
done
|
|
printf '%s' "${t:-60s}"
|
|
}
|
|
|
|
# Mirrors the proxy directives from dokku's default nginx template so
|
|
# requests routed through our locations behave like stock dokku routing.
|
|
fn-ga-proxy-directives() {
|
|
declare UPSTREAM="$1" TIMEOUT="$2"
|
|
cat <<EOF
|
|
proxy_pass http://${UPSTREAM};
|
|
proxy_http_version 1.1;
|
|
proxy_read_timeout ${TIMEOUT};
|
|
proxy_set_header Upgrade \$http_upgrade;
|
|
proxy_set_header Connection \$http_connection;
|
|
proxy_set_header Host \$http_host;
|
|
proxy_set_header X-Forwarded-For \$remote_addr;
|
|
proxy_set_header X-Forwarded-Port \$server_port;
|
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
proxy_set_header X-Request-Start \$msec;
|
|
EOF
|
|
}
|
|
|
|
fn-ga-generate-conf() {
|
|
declare APP="$1"
|
|
local upstream timeout port
|
|
upstream="$(fn-ga-upstream-name "$APP")" || return 1
|
|
timeout="$(fn-ga-proxy-read-timeout "$APP")"
|
|
port="$(fn-ga-global-get port "$GOOGLE_AUTH_DEFAULT_PORT")"
|
|
|
|
cat <<EOF
|
|
# Managed by the dokku google-auth plugin — do not edit by hand.
|
|
# Regenerated on every deploy and by google-auth:* commands.
|
|
|
|
# Every location below sets X-Google-Auth-App explicitly, which both tells the
|
|
# auth service whose access lists to apply and overwrites any value a client
|
|
# tried to send.
|
|
location = ${GOOGLE_AUTH_ROUTE_PREFIX}/verify {
|
|
internal;
|
|
proxy_pass http://127.0.0.1:${port};
|
|
proxy_pass_request_body off;
|
|
proxy_set_header Content-Length "";
|
|
proxy_set_header Host \$host;
|
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
proxy_set_header X-Forwarded-For \$remote_addr;
|
|
proxy_set_header X-Google-Auth-App "${APP}";
|
|
}
|
|
|
|
location ^~ ${GOOGLE_AUTH_ROUTE_PREFIX}/ {
|
|
proxy_pass http://127.0.0.1:${port};
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host \$host;
|
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
proxy_set_header X-Forwarded-For \$remote_addr;
|
|
proxy_set_header X-Forwarded-Port \$server_port;
|
|
proxy_set_header X-Auth-Request-Redirect "";
|
|
proxy_set_header X-Google-Auth-App "${APP}";
|
|
}
|
|
|
|
location @google_auth_signin {
|
|
rewrite ^ ${GOOGLE_AUTH_ROUTE_PREFIX}/start break;
|
|
proxy_pass http://127.0.0.1:${port};
|
|
proxy_set_header Host \$host;
|
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
proxy_set_header X-Forwarded-For \$remote_addr;
|
|
proxy_set_header X-Auth-Request-Redirect \$request_uri;
|
|
proxy_set_header X-Google-Auth-App "${APP}";
|
|
}
|
|
EOF
|
|
|
|
local pattern
|
|
while IFS= read -r pattern; do
|
|
[[ -z "$pattern" ]] && continue
|
|
echo ""
|
|
if [[ "$pattern" == re:* ]]; then
|
|
echo "# google-auth: path excluded from SSO (${pattern})"
|
|
echo "location ~ ${pattern#re:} {"
|
|
else
|
|
echo "# google-auth: path excluded from SSO (${pattern})"
|
|
echo "location ^~ ${pattern} {"
|
|
fi
|
|
fn-ga-proxy-directives "$upstream" "$timeout"
|
|
cat <<'EOF'
|
|
# Strip identity headers so clients cannot spoof them on open paths.
|
|
proxy_set_header X-Forwarded-User "";
|
|
proxy_set_header X-Forwarded-Email "";
|
|
proxy_set_header X-Auth-Request-User "";
|
|
proxy_set_header X-Auth-Request-Email "";
|
|
proxy_set_header X-Auth-Request-Name "";
|
|
proxy_set_header X-Google-Auth-App "";
|
|
}
|
|
EOF
|
|
done < <(fn-ga-excludes "$APP")
|
|
|
|
cat <<EOF
|
|
|
|
# Everything else requires a Google session.
|
|
location ~ ^/ {
|
|
auth_request ${GOOGLE_AUTH_ROUTE_PREFIX}/verify;
|
|
auth_request_set \$google_auth_user \$upstream_http_x_auth_request_user;
|
|
auth_request_set \$google_auth_email \$upstream_http_x_auth_request_email;
|
|
auth_request_set \$google_auth_name \$upstream_http_x_auth_request_name;
|
|
error_page 401 = @google_auth_signin;
|
|
|
|
$(fn-ga-proxy-directives "$upstream" "$timeout")
|
|
proxy_set_header X-Forwarded-User \$google_auth_user;
|
|
proxy_set_header X-Forwarded-Email \$google_auth_email;
|
|
proxy_set_header X-Auth-Request-User \$google_auth_user;
|
|
proxy_set_header X-Auth-Request-Email \$google_auth_email;
|
|
proxy_set_header X-Auth-Request-Name \$google_auth_name;
|
|
proxy_set_header X-Google-Auth-App "";
|
|
}
|
|
EOF
|
|
}
|
|
|
|
fn-ga-conf-path() {
|
|
declare APP="$1"
|
|
echo "$DOKKU_ROOT/$APP/nginx.conf.d/google-auth.conf"
|
|
}
|
|
|
|
# Regenerates (or removes) the app's conf file without touching nginx.
|
|
# Prints one of: changed, unchanged, skipped.
|
|
fn-ga-write-conf() {
|
|
declare APP="$1"
|
|
local conf dir tmp
|
|
conf="$(fn-ga-conf-path "$APP")"
|
|
dir="$(dirname "$conf")"
|
|
|
|
if ! fn-google-auth-app-enabled "$APP"; then
|
|
if [[ -f "$conf" ]]; then
|
|
rm -f "$conf"
|
|
echo changed
|
|
else
|
|
echo unchanged
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
tmp="$(mktemp)"
|
|
if ! fn-ga-generate-conf "$APP" >"$tmp" 2>/dev/null; then
|
|
rm -f "$tmp"
|
|
echo skipped
|
|
return 0
|
|
fi
|
|
mkdir -p "$dir"
|
|
if [[ -f "$conf" ]] && cmp -s "$tmp" "$conf"; then
|
|
rm -f "$tmp"
|
|
echo unchanged
|
|
return 0
|
|
fi
|
|
cat "$tmp" >"$conf"
|
|
rm -f "$tmp"
|
|
echo changed
|
|
}
|
|
|
|
# --- nginx validate/reload (via dokku core helpers when available) ---
|
|
|
|
fn-ga-source-nginx-functions() {
|
|
if [[ -f "$PLUGIN_CORE_AVAILABLE_PATH/nginx-vhosts/functions" ]]; then
|
|
# shellcheck disable=SC1091
|
|
source "$PLUGIN_CORE_AVAILABLE_PATH/nginx-vhosts/functions" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
fn-ga-nginx-validate() {
|
|
fn-ga-source-nginx-functions
|
|
if declare -f validate_nginx >/dev/null 2>&1; then
|
|
(validate_nginx) >/dev/null 2>&1
|
|
return $?
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
fn-ga-nginx-reload() {
|
|
fn-ga-source-nginx-functions
|
|
if declare -f restart_nginx >/dev/null 2>&1; then
|
|
restart_nginx >/dev/null 2>&1 || dokku_log_warn "nginx reload reported an error; check 'nginx -t'"
|
|
else
|
|
dokku_log_warn "could not reload nginx automatically; run: sudo systemctl reload nginx"
|
|
fi
|
|
}
|
|
|
|
# Write conf for one app, validate nginx, roll back on failure, reload.
|
|
fn-ga-apply() {
|
|
declare APP="$1"
|
|
local conf backup="" had_file=false status
|
|
conf="$(fn-ga-conf-path "$APP")"
|
|
if [[ -f "$conf" ]]; then
|
|
backup="$(mktemp)"
|
|
cat "$conf" >"$backup"
|
|
had_file=true
|
|
fi
|
|
|
|
status="$(fn-ga-write-conf "$APP")"
|
|
case "$status" in
|
|
skipped)
|
|
dokku_log_warn "$APP has no generated nginx config yet (not deployed?). google-auth config will be added on the next deploy."
|
|
;;
|
|
changed)
|
|
if ! fn-ga-nginx-validate; then
|
|
if [[ "$had_file" == "true" ]]; then
|
|
cat "$backup" >"$conf"
|
|
else
|
|
rm -f "$conf"
|
|
fi
|
|
[[ -n "$backup" ]] && rm -f "$backup"
|
|
dokku_log_fail "nginx rejected the generated config for $APP; change reverted (check exclude patterns)"
|
|
fi
|
|
fn-ga-nginx-reload
|
|
;;
|
|
esac
|
|
[[ -n "$backup" ]] && rm -f "$backup"
|
|
return 0
|
|
}
|
|
|
|
# Regenerate confs for every enabled app (e.g. after the service port changes).
|
|
fn-ga-apply-all() {
|
|
local app any_changed=false status
|
|
while IFS= read -r app; do
|
|
[[ -z "$app" ]] && continue
|
|
status="$(fn-ga-write-conf "$app")"
|
|
[[ "$status" == "changed" ]] && any_changed=true
|
|
done < <(fn-ga-enabled-apps)
|
|
if [[ "$any_changed" == "true" ]]; then
|
|
if fn-ga-nginx-validate; then
|
|
fn-ga-nginx-reload
|
|
else
|
|
dokku_log_warn "nginx validation failed after regenerating google-auth configs; run 'nginx -t' to inspect"
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# --- auth service container management ---
|
|
|
|
fn-ga-image-exists() {
|
|
docker image inspect "$GOOGLE_AUTH_IMAGE" >/dev/null 2>&1
|
|
}
|
|
|
|
fn-ga-build-image() {
|
|
command -v docker >/dev/null 2>&1 || dokku_log_fail "docker is required to build the google-auth service image"
|
|
dokku_log_info1 "Building $GOOGLE_AUTH_IMAGE (first build downloads the Go toolchain image; this can take a few minutes)"
|
|
docker image build -t "$GOOGLE_AUTH_IMAGE" "$GOOGLE_AUTH_PLUGIN_DIR"
|
|
}
|
|
|
|
fn-ga-service-running() {
|
|
[[ "$(docker container inspect -f '{{.State.Running}}' "$GOOGLE_AUTH_SERVICE_NAME" 2>/dev/null)" == "true" ]]
|
|
}
|
|
|
|
# True when the running container has the per-app config mount. A container
|
|
# started by an older version of this plugin will not, and would silently
|
|
# ignore per-app lists.
|
|
fn-ga-service-has-app-mount() {
|
|
docker container inspect -f '{{range .Mounts}}{{println .Destination}}{{end}}' \
|
|
"$GOOGLE_AUTH_SERVICE_NAME" 2>/dev/null | grep -qxF "$GOOGLE_AUTH_APP_CONFIG_MOUNT"
|
|
}
|
|
|
|
fn-ga-write-env-file() {
|
|
local envfile="$GOOGLE_AUTH_DATA_ROOT/service.env"
|
|
local domains emails denied
|
|
domains="$(fn-ga-global-get-list allowed-domains | paste -sd, -)"
|
|
emails="$(fn-ga-global-get-list allowed-emails | paste -sd, -)"
|
|
denied="$(fn-ga-global-get-list denied-emails | paste -sd, -)"
|
|
mkdir -p "$GOOGLE_AUTH_DATA_ROOT"
|
|
umask 077
|
|
cat >"$envfile" <<EOF
|
|
GOOGLE_AUTH_CLIENT_ID=$(fn-ga-global-get client-id)
|
|
GOOGLE_AUTH_CLIENT_SECRET=$(fn-ga-global-get client-secret)
|
|
GOOGLE_AUTH_COOKIE_SECRET=$(fn-ga-global-get cookie-secret)
|
|
GOOGLE_AUTH_AUTH_HOST=$(fn-ga-global-get auth-host)
|
|
GOOGLE_AUTH_ALLOWED_DOMAINS=$domains
|
|
GOOGLE_AUTH_ALLOWED_EMAILS=$emails
|
|
GOOGLE_AUTH_DENIED_EMAILS=$denied
|
|
GOOGLE_AUTH_APP_CONFIG_DIR=$GOOGLE_AUTH_APP_CONFIG_MOUNT
|
|
GOOGLE_AUTH_COOKIE_NAME=$(fn-ga-global-get cookie-name _google_auth)
|
|
GOOGLE_AUTH_SESSION_TTL=$(fn-ga-global-get session-ttl 24h)
|
|
GOOGLE_AUTH_ALLOW_INSECURE=$(fn-ga-global-get allow-insecure false)
|
|
GOOGLE_AUTH_LISTEN=:2999
|
|
EOF
|
|
}
|
|
|
|
fn-ga-service-start() {
|
|
fn-ga-configured || dokku_log_fail "google-auth is not fully configured; run: dokku google-auth:configure"
|
|
fn-ga-image-exists || fn-ga-build-image
|
|
fn-ga-write-env-file
|
|
local port
|
|
port="$(fn-ga-global-get port "$GOOGLE_AUTH_DEFAULT_PORT")"
|
|
mkdir -p "$GOOGLE_AUTH_DATA_ROOT/apps"
|
|
chmod 711 "$GOOGLE_AUTH_DATA_ROOT/apps" 2>/dev/null || true
|
|
docker container rm -f "$GOOGLE_AUTH_SERVICE_NAME" >/dev/null 2>&1 || true
|
|
docker container run -d \
|
|
--name "$GOOGLE_AUTH_SERVICE_NAME" \
|
|
--restart=unless-stopped \
|
|
-p "127.0.0.1:${port}:2999" \
|
|
--env-file "$GOOGLE_AUTH_DATA_ROOT/service.env" \
|
|
-v "$GOOGLE_AUTH_DATA_ROOT/apps:${GOOGLE_AUTH_APP_CONFIG_MOUNT}:ro" \
|
|
"$GOOGLE_AUTH_IMAGE" >/dev/null
|
|
dokku_log_info1 "google-auth service running on 127.0.0.1:${port}"
|
|
}
|
|
|
|
fn-ga-service-stop() {
|
|
docker container rm -f "$GOOGLE_AUTH_SERVICE_NAME" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
# Warn when the configured auth host is not served by any enabled app,
|
|
# because Google's callback would then land on nothing.
|
|
fn-ga-warn-if-auth-host-unrouted() {
|
|
local auth_host app vhost_file
|
|
auth_host="$(fn-ga-global-get auth-host)"
|
|
[[ -n "$auth_host" ]] || return 0
|
|
while IFS= read -r app; do
|
|
[[ -z "$app" ]] && continue
|
|
vhost_file="$DOKKU_ROOT/$app/VHOST"
|
|
[[ -f "$vhost_file" ]] && grep -qxF "$auth_host" "$vhost_file" && return 0
|
|
done < <(fn-ga-enabled-apps)
|
|
dokku_log_warn "auth host '$auth_host' is not a domain of any google-auth-enabled app."
|
|
dokku_log_warn "Google's OAuth callback (https://$auth_host$GOOGLE_AUTH_ROUTE_PREFIX/callback) must route to an enabled app."
|
|
return 0
|
|
}
|