Allow per-app allow/deny.

This commit is contained in:
Greyson Parrelli
2026-08-06 13:53:24 -04:00
parent c15a1cc14c
commit 8ce2919627
19 changed files with 1151 additions and 187 deletions
+164 -16
View File
@@ -19,6 +19,8 @@ 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
@@ -73,19 +75,57 @@ fn-ga-global-set-list() {
chmod 600 "$file"
}
# Additive counterparts to fn-ga-global-set-list, for the allow/deny commands.
fn-ga-global-list-add() {
declare KEY="$1" VALUE="$2"
mkdir -p "$GOOGLE_AUTH_DATA_ROOT/global"
local file="$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
touch "$file"
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-global-list-remove() {
declare KEY="$1" VALUE="$2"
local file="$GOOGLE_AUTH_DATA_ROOT/global/$KEY" tmp
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
@@ -93,14 +133,88 @@ fn-ga-global-list-remove() {
rm -f "$tmp"
}
fn-ga-global-list-contains() {
declare KEY="$1" VALUE="$2"
grep -qxF "$VALUE" "$GOOGLE_AUTH_DATA_ROOT/global/$KEY" 2>/dev/null
# 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
}
fn-ga-global-list-count() {
declare KEY="$1"
fn-ga-global-get-list "$KEY" | grep -c . || true
# 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
@@ -137,6 +251,20 @@ fn-ga-app-dir() {
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" ]]
@@ -272,6 +400,9 @@ fn-ga-generate-conf() {
# 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};
@@ -280,6 +411,7 @@ location = ${GOOGLE_AUTH_ROUTE_PREFIX}/verify {
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}/ {
@@ -290,6 +422,7 @@ location ^~ ${GOOGLE_AUTH_ROUTE_PREFIX}/ {
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 {
@@ -299,6 +432,7 @@ location @google_auth_signin {
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
@@ -321,6 +455,7 @@ EOF
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")
@@ -341,6 +476,7 @@ $(fn-ga-proxy-directives "$upstream" "$timeout")
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
}
@@ -479,6 +615,14 @@ 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
@@ -495,6 +639,7 @@ 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)
@@ -508,12 +653,15 @@ fn-ga-service-start() {
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}"
}