Rewrite onboarding prose to a neutral voice
This commit is contained in:
@@ -23,7 +23,7 @@ FSDGG_CLI_CLIENT_ID="ba4ec9ec-8ae8-4450-9cec-fd532bbe63d5"
|
|||||||
# http-scheme loopback: http://127.0.0.1:<port>/<path>,
|
# http-scheme loopback: http://127.0.0.1:<port>/<path>,
|
||||||
# http://[::1]:<port>/<path>, or http://localhost:<port>/<path>.
|
# http://[::1]:<port>/<path>, or http://localhost:<port>/<path>.
|
||||||
# The value must also match the redirect URI registered in the Gitea
|
# The value must also match the redirect URI registered in the Gitea
|
||||||
# OAuth app; change it only if your Gitea app registration was updated
|
# OAuth app; change it only if the Gitea app registration was updated
|
||||||
# in sync.
|
# in sync.
|
||||||
FSDGG_CLI_REDIRECT_URI="http://127.0.0.1:38111/callback"
|
FSDGG_CLI_REDIRECT_URI="http://127.0.0.1:38111/callback"
|
||||||
|
|
||||||
|
|||||||
12
Justfile
12
Justfile
@@ -53,7 +53,7 @@ check-gitea:
|
|||||||
@echo "[check-gitea] GET $FORGE_GITEA_URL/api/v1/version"
|
@echo "[check-gitea] GET $FORGE_GITEA_URL/api/v1/version"
|
||||||
@curl -fsS --max-time 10 ${FORGE_INSECURE_TLS:+-k} "$FORGE_GITEA_URL/api/v1/version" \
|
@curl -fsS --max-time 10 ${FORGE_INSECURE_TLS:+-k} "$FORGE_GITEA_URL/api/v1/version" \
|
||||||
| python3 -c 'import json,sys; d=json.load(sys.stdin); print("[check-gitea] Gitea version:", d.get("version","?"))'
|
| python3 -c 'import json,sys; d=json.load(sys.stdin); print("[check-gitea] Gitea version:", d.get("version","?"))'
|
||||||
@echo "[check-gitea] OK -- you can reach Gitea."
|
@echo "[check-gitea] OK: Gitea reachable."
|
||||||
|
|
||||||
# Browser OAuth2 (PKCE) login. Reuses a live token; runs the flow otherwise.
|
# Browser OAuth2 (PKCE) login. Reuses a live token; runs the flow otherwise.
|
||||||
login:
|
login:
|
||||||
@@ -91,12 +91,12 @@ check-access:
|
|||||||
DISPLAY='' WAYLAND_DISPLAY='' \
|
DISPLAY='' WAYLAND_DISPLAY='' \
|
||||||
git ls-remote "$FORGE_ORCHESTRATOR_REPO_URL" HEAD >/dev/null 2>&1 \
|
git ls-remote "$FORGE_ORCHESTRATOR_REPO_URL" HEAD >/dev/null 2>&1 \
|
||||||
&& { \
|
&& { \
|
||||||
echo "[check-access] OK -- you can reach the orchestrator."; \
|
echo "[check-access] OK: orchestrator reachable."; \
|
||||||
} || { \
|
} || { \
|
||||||
echo "[check-access] FAILED. Likely causes:"; \
|
echo "[check-access] FAILED. Likely causes:"; \
|
||||||
echo " - you have not run 'just login' yet"; \
|
echo " - 'just login' has not completed yet"; \
|
||||||
echo " - your refresh token expired; run 'just relogin'"; \
|
echo " - stored refresh token expired; run 'just relogin'"; \
|
||||||
echo " - your Gitea account is not yet in the org"; \
|
echo " - Gitea account is not yet in the org"; \
|
||||||
echo " - FORGE_ORCHESTRATOR_REPO_URL in .env is wrong"; \
|
echo " - FORGE_ORCHESTRATOR_REPO_URL in .env is wrong"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
}
|
}
|
||||||
@@ -141,7 +141,7 @@ next-steps *args:
|
|||||||
run-next-steps *args:
|
run-next-steps *args:
|
||||||
@bash scripts/next_steps.sh --run {{args}}
|
@bash scripts/next_steps.sh --run {{args}}
|
||||||
|
|
||||||
# Remove the credential helper and our fields from client-auth.json.
|
# Remove the credential helper and repo-managed fields from client-auth.json.
|
||||||
uninstall:
|
uninstall:
|
||||||
@bash scripts/uninstall.sh
|
@bash scripts/uninstall.sh
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ edit is `FORGE_GITEA_USERNAME`. Defaults:
|
|||||||
|
|
||||||
**`FSDGG_CLI_REDIRECT_URI` is not the Gitea URL.** The Gitea server is
|
**`FSDGG_CLI_REDIRECT_URI` is not the Gitea URL.** The Gitea server is
|
||||||
at `FORGE_GITEA_URL` (remote, HTTPS). The redirect URI is the local
|
at `FORGE_GITEA_URL` (remote, HTTPS). The redirect URI is the local
|
||||||
loopback HTTP listener the CLI binds on *your* machine so Gitea can
|
loopback HTTP listener the CLI binds on the local machine so Gitea can
|
||||||
hand back the OAuth authorisation code; OAuth 2.0 for Native Apps
|
hand back the OAuth authorisation code; OAuth 2.0 for Native Apps
|
||||||
(RFC 8252 §7.3) prohibits any non-loopback / non-HTTP scheme here,
|
(RFC 8252 §7.3) prohibits any non-loopback / non-HTTP scheme here,
|
||||||
and no public CA will issue a cert for `127.0.0.1` so HTTPS on
|
and no public CA will issue a cert for `127.0.0.1` so HTTPS on
|
||||||
@@ -217,7 +217,7 @@ with a browser to populate a valid refresh token before running
|
|||||||
| Git prompts for a password on pull/push | Refresh token expired. Run `just relogin`. |
|
| Git prompts for a password on pull/push | Refresh token expired. Run `just relogin`. |
|
||||||
| `just status` shows `live: False` | Run `just refresh`; also happens automatically on the next git op. |
|
| `just status` shows `live: False` | Run `just refresh`; also happens automatically on the next git op. |
|
||||||
| `just clone-orchestrator` prints `already cloned` | Intended; idempotent. |
|
| `just clone-orchestrator` prints `already cloned` | Intended; idempotent. |
|
||||||
| Want a clean slate | `just uninstall`. |
|
| Reset local state | `just uninstall`. |
|
||||||
|
|
||||||
## Security properties
|
## Security properties
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ repo_root() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Load $repo/.env into the environment.
|
# Load $repo/.env into the environment.
|
||||||
# Existing environment values take precedence (matches just's dotenv-load).
|
# Existing environment values take precedence (matches dotenv-load in just).
|
||||||
load_env() {
|
load_env() {
|
||||||
local root env line key rest
|
local root env line key rest
|
||||||
root="$(repo_root)"
|
root="$(repo_root)"
|
||||||
@@ -111,6 +111,6 @@ require_cmd() {
|
|||||||
require_env() {
|
require_env() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
if [ -z "${!name:-}" ]; then
|
if [ -z "${!name:-}" ]; then
|
||||||
die "environment variable '$name' is unset. Run 'just init-env' and fill .env."
|
die "environment variable '$name' is unset. Run 'just init-env'. Fill .env."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ if command -v xdg-open >/dev/null 2>&1 || command -v open >/dev/null 2>&1; then
|
|||||||
printf ' %s %-12s -> (detected)\n' "$(_fc_tag "$_FC_GREEN" ok)" 'web browser'
|
printf ' %s %-12s -> (detected)\n' "$(_fc_tag "$_FC_GREEN" ok)" 'web browser'
|
||||||
else
|
else
|
||||||
printf ' %s %-12s -> no xdg-open/open on PATH.\n' "$(_fc_tag "$_FC_YELLOW" warn)" 'web browser'
|
printf ' %s %-12s -> no xdg-open/open on PATH.\n' "$(_fc_tag "$_FC_YELLOW" warn)" 'web browser'
|
||||||
printf ' if this is a headless machine, run: just login-headless\n'
|
printf ' headless mode: just login-headless\n'
|
||||||
case "$pm" in
|
case "$pm" in
|
||||||
apt) printf ' otherwise install: sudo apt-get install -y xdg-utils\n' ;;
|
apt) printf ' otherwise install: sudo apt-get install -y xdg-utils\n' ;;
|
||||||
dnf) printf ' otherwise install: sudo dnf install -y xdg-utils\n' ;;
|
dnf) printf ' otherwise install: sudo dnf install -y xdg-utils\n' ;;
|
||||||
@@ -214,7 +214,7 @@ if [ "$missing" -gt 0 ]; then
|
|||||||
printf ' %s\n' "$c"
|
printf ' %s\n' "$c"
|
||||||
done
|
done
|
||||||
printf '\n'
|
printf '\n'
|
||||||
warn 'then re-run: just doctor'
|
warn 're-run: just doctor'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -282,11 +282,12 @@ def _http_post_form(
|
|||||||
|
|
||||||
def discover_endpoints(config: ForgeAuthConfig) -> dict[str, str]:
|
def discover_endpoints(config: ForgeAuthConfig) -> dict[str, str]:
|
||||||
"""Call Gitea's ``/.well-known/openid-configuration`` and return the
|
"""Call Gitea's ``/.well-known/openid-configuration`` and return the
|
||||||
three endpoints we care about, validated against the issuer.
|
three required endpoints, validated against the issuer.
|
||||||
"""
|
"""
|
||||||
url = f"{config.gitea_base_url}/.well-known/openid-configuration"
|
url = f"{config.gitea_base_url}/.well-known/openid-configuration"
|
||||||
payload = _http_get_json(url, insecure_tls=config.insecure_tls)
|
payload = _http_get_json(url, insecure_tls=config.insecure_tls)
|
||||||
issuer = payload.get("issuer")
|
issuer = payload.get("issuer")
|
||||||
|
|
||||||
if not isinstance(issuer, str) or issuer.rstrip("/") != config.gitea_base_url.rstrip("/"):
|
if not isinstance(issuer, str) or issuer.rstrip("/") != config.gitea_base_url.rstrip("/"):
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
f"OIDC discovery issuer {issuer!r} does not match "
|
f"OIDC discovery issuer {issuer!r} does not match "
|
||||||
@@ -401,7 +402,7 @@ class _CallbackHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(
|
self.wfile.write(
|
||||||
b"<html><body><h1>Login failed</h1><p>You can close this window.</p></body></html>"
|
b"<html><body><h1>Login failed</h1><p>Window close is safe.</p></body></html>"
|
||||||
)
|
)
|
||||||
server.result_queue.put(
|
server.result_queue.put(
|
||||||
_build_authorize_error(
|
_build_authorize_error(
|
||||||
@@ -426,7 +427,7 @@ class _CallbackHandler(BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(
|
self.wfile.write(
|
||||||
b"<html><body style='font-family:sans-serif'>"
|
b"<html><body style='font-family:sans-serif'>"
|
||||||
b"<h1>Login complete.</h1>"
|
b"<h1>Login complete.</h1>"
|
||||||
b"<p>You can close this window and return to your terminal.</p>"
|
b"<p>Window close is safe. Terminal focus can resume.</p>"
|
||||||
b"</body></html>"
|
b"</body></html>"
|
||||||
)
|
)
|
||||||
server.result_queue.put((code, state))
|
server.result_queue.put((code, state))
|
||||||
@@ -471,7 +472,7 @@ def _build_authorize_error(
|
|||||||
gitea_base_url : str
|
gitea_base_url : str
|
||||||
Base URL of the Gitea server. Used to build the user-settings
|
Base URL of the Gitea server. Used to build the user-settings
|
||||||
URL in the remediation message. Empty string yields a
|
URL in the remediation message. Empty string yields a
|
||||||
``<your-gitea-url>`` placeholder.
|
``<gitea-base-url>`` placeholder.
|
||||||
client_id : str, optional
|
client_id : str, optional
|
||||||
OAuth client id requesting the authorization. Surfaced in the
|
OAuth client id requesting the authorization. Surfaced in the
|
||||||
"different scope" message to disambiguate the grant row in
|
"different scope" message to disambiguate the grant row in
|
||||||
@@ -493,7 +494,7 @@ def _build_authorize_error(
|
|||||||
settings_url = (
|
settings_url = (
|
||||||
f"{base}/user/settings/applications"
|
f"{base}/user/settings/applications"
|
||||||
if base
|
if base
|
||||||
else "<your-gitea-url>/user/settings/applications"
|
else "<gitea-base-url>/user/settings/applications"
|
||||||
)
|
)
|
||||||
|
|
||||||
if "different scope" in low or ("scope" in low and "grant" in low):
|
if "different scope" in low or ("scope" in low and "grant" in low):
|
||||||
@@ -554,8 +555,8 @@ def wait_for_callback(
|
|||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
f"cannot bind loopback server on {host}:{port}: {exc}. "
|
f"cannot bind loopback server on {host}:{port}: {exc}. "
|
||||||
f"Is another 'just login' still running? "
|
f"Another 'just login' instance may still be running. "
|
||||||
f"If port {port} is held by an unrelated process, override "
|
f"If port {port} is held by an unrelated process, set "
|
||||||
f"FSDGG_CLI_REDIRECT_URI in .env (note: the port must match "
|
f"FSDGG_CLI_REDIRECT_URI in .env (note: the port must match "
|
||||||
f"the OAuth app registered in Gitea)."
|
f"the OAuth app registered in Gitea)."
|
||||||
) from exc
|
) from exc
|
||||||
@@ -566,7 +567,7 @@ def wait_for_callback(
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
f"timed out after {int(timeout_seconds)}s waiting for OAuth "
|
f"timed out after {int(timeout_seconds)}s waiting for OAuth "
|
||||||
f"callback. Did you complete the browser login?"
|
f"callback. Browser login completion was not detected."
|
||||||
) from exc
|
) from exc
|
||||||
finally:
|
finally:
|
||||||
server.server_close()
|
server.server_close()
|
||||||
@@ -763,18 +764,17 @@ def _print_headless_guidance(auth_url: str, redirect) -> None:
|
|||||||
]
|
]
|
||||||
if in_ssh:
|
if in_ssh:
|
||||||
lines += [
|
lines += [
|
||||||
f"{info_tag} this process is running inside an SSH session. From the",
|
f"{info_tag} SSH session detected. From the browser-side machine run:",
|
||||||
" machine where you will open the browser, run:",
|
|
||||||
"",
|
"",
|
||||||
f" ssh -L {cb_port}:127.0.0.1:{cb_port} {user}@{hostname}",
|
f" ssh -L {cb_port}:127.0.0.1:{cb_port} {user}@{hostname}",
|
||||||
"",
|
"",
|
||||||
" and paste the URL above into THAT machine's browser.",
|
" Paste the URL above into that machine's browser.",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
lines += [
|
lines += [
|
||||||
f"{info_tag} if this machine is remote, from the machine with the",
|
f"{info_tag} Remote-host case: from the browser-side machine run",
|
||||||
" browser run (before pasting the URL):",
|
" the following command before pasting the URL:",
|
||||||
"",
|
"",
|
||||||
f" ssh -L {cb_port}:127.0.0.1:{cb_port} {user}@{hostname}",
|
f" ssh -L {cb_port}:127.0.0.1:{cb_port} {user}@{hostname}",
|
||||||
"",
|
"",
|
||||||
@@ -825,8 +825,8 @@ def run_login(
|
|||||||
if print_authorize_url:
|
if print_authorize_url:
|
||||||
if open_browser:
|
if open_browser:
|
||||||
cli_info(
|
cli_info(
|
||||||
f"open this URL in your browser if it does not open "
|
f"authorization URL (fallback when automatic browser open fails):\n"
|
||||||
f"automatically:\n {auth_url}"
|
f" {auth_url}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_print_headless_guidance(auth_url, redirect)
|
_print_headless_guidance(auth_url, redirect)
|
||||||
@@ -882,7 +882,7 @@ def run_login(
|
|||||||
f"'{config.expected_username}'. The stored auth file has "
|
f"'{config.expected_username}'. The stored auth file has "
|
||||||
"NOT been updated.\n"
|
"NOT been updated.\n"
|
||||||
" To fix:\n"
|
" To fix:\n"
|
||||||
f" 1. Sign out of Gitea in your browser: {logout_url}\n"
|
f" 1. Sign out of Gitea in the browser: {logout_url}\n"
|
||||||
f" 2. Sign back in as '{config.expected_username}'\n"
|
f" 2. Sign back in as '{config.expected_username}'\n"
|
||||||
" 3. Re-run 'just login'"
|
" 3. Re-run 'just login'"
|
||||||
)
|
)
|
||||||
@@ -905,8 +905,7 @@ def run_refresh(config: ForgeAuthConfig, *, must_refresh: bool = False) -> AuthF
|
|||||||
If ``must_refresh`` is False and the access token is still live,
|
If ``must_refresh`` is False and the access token is still live,
|
||||||
this is a no-op. If refresh fails, raises ``AuthError`` (the
|
this is a no-op. If refresh fails, raises ``AuthError`` (the
|
||||||
credential helper surfaces this to git as "fall through to prompt",
|
credential helper surfaces this to git as "fall through to prompt",
|
||||||
and the CLI ``just refresh`` surfaces it with an instruction to
|
and the CLI ``just refresh`` surfaces the failure to stderr).
|
||||||
re-run ``just login``).
|
|
||||||
"""
|
"""
|
||||||
store = auth_store_path()
|
store = auth_store_path()
|
||||||
existing = AuthFile.read(store)
|
existing = AuthFile.read(store)
|
||||||
@@ -934,7 +933,7 @@ def run_refresh(config: ForgeAuthConfig, *, must_refresh: bool = False) -> AuthF
|
|||||||
def run_logout() -> Path | None:
|
def run_logout() -> Path | None:
|
||||||
"""Remove every codevalet-managed field from the auth file.
|
"""Remove every codevalet-managed field from the auth file.
|
||||||
|
|
||||||
If the file only ever held our fields, delete it entirely. If the
|
If the file only ever held repo-managed fields, delete it entirely. If the
|
||||||
gateway has already populated its own fields (access_token etc.),
|
gateway has already populated its own fields (access_token etc.),
|
||||||
wipe only the Gitea + welcome-repo keys and leave the rest.
|
wipe only the Gitea + welcome-repo keys and leave the rest.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ python3 "$here/forge_auth.py" login "$@"
|
|||||||
"$here/install-git-credential-helper.sh"
|
"$here/install-git-credential-helper.sh"
|
||||||
|
|
||||||
ok "login complete"
|
ok "login complete"
|
||||||
note "git operations against your Gitea server now authenticate silently."
|
note "git operations against FORGE_GITEA_URL now authenticate without prompts."
|
||||||
note "Test with:"
|
note "Verification command:"
|
||||||
note " git ls-remote \"\$FORGE_ORCHESTRATOR_REPO_URL\""
|
note " git ls-remote \"\$FORGE_ORCHESTRATOR_REPO_URL\""
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ On every ``get`` the helper:
|
|||||||
unchanged so the normal prompt chain continues.
|
unchanged so the normal prompt chain continues.
|
||||||
2. Checks that the git request host matches the host recorded in
|
2. Checks that the git request host matches the host recorded in
|
||||||
``_forge_gitea_base_url`` (or ``FORGE_GITEA_URL``). If not, passes
|
``_forge_gitea_base_url`` (or ``FORGE_GITEA_URL``). If not, passes
|
||||||
through: this ensures we never hand OAuth tokens to unrelated
|
through: this prevents OAuth token disclosure to unrelated
|
||||||
hosts even if git mis-scopes its lookup.
|
hosts even if git mis-scopes its lookup.
|
||||||
3. If ``gitea_access_token`` is live, emits
|
3. If ``gitea_access_token`` is live, emits
|
||||||
``username=<stored-user>`` and ``password=<gitea_access_token>``.
|
``username=<stored-user>`` and ``password=<gitea_access_token>``.
|
||||||
@@ -29,7 +29,7 @@ Security notes
|
|||||||
--------------
|
--------------
|
||||||
* The helper never writes to stdout except the credential key=value
|
* The helper never writes to stdout except the credential key=value
|
||||||
block. Logs go to stderr.
|
block. Logs go to stderr.
|
||||||
* On OAuth refresh failure we exit **non-zero** rather than silently
|
* On OAuth refresh failure the process exits **non-zero** rather than silently
|
||||||
returning stale credentials.
|
returning stale credentials.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -93,7 +93,7 @@ def _configured_host() -> tuple[str, str, int | None] | None:
|
|||||||
|
|
||||||
Priority:
|
Priority:
|
||||||
1. The ``_forge_gitea_base_url`` field inside the stored auth
|
1. The ``_forge_gitea_base_url`` field inside the stored auth
|
||||||
file: that is the host we actually authenticated against.
|
file: the authenticated host.
|
||||||
2. The ``FORGE_GITEA_URL`` env var (pre-login override).
|
2. The ``FORGE_GITEA_URL`` env var (pre-login override).
|
||||||
Returns None if neither is set; the helper then passes through.
|
Returns None if neither is set; the helper then passes through.
|
||||||
"""
|
"""
|
||||||
@@ -150,7 +150,7 @@ def _request_matches(
|
|||||||
def cmd_get(fields: dict[str, str]) -> int:
|
def cmd_get(fields: dict[str, str]) -> int:
|
||||||
configured = _configured_host()
|
configured = _configured_host()
|
||||||
if configured is None:
|
if configured is None:
|
||||||
emit(fields) # pass-through: we don't know who we answer for
|
emit(fields) # pass-through: helper scope is undefined
|
||||||
return 0
|
return 0
|
||||||
if not _request_matches(fields, configured):
|
if not _request_matches(fields, configured):
|
||||||
emit(fields) # pass-through: request is for a different host
|
emit(fields) # pass-through: request is for a different host
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ python3 -c 'import sys; compile(open(sys.argv[1]).read(), sys.argv[1], "exec")'
|
|||||||
key="credential.${FORGE_GITEA_URL}.helper"
|
key="credential.${FORGE_GITEA_URL}.helper"
|
||||||
use_path_key="credential.${FORGE_GITEA_URL}.useHttpPath"
|
use_path_key="credential.${FORGE_GITEA_URL}.useHttpPath"
|
||||||
|
|
||||||
# Wipe any previous helpers we may have set so we do not stack them.
|
# Wipe previously configured helpers to avoid stacking entries.
|
||||||
git config --global --unset-all "$key" 2>/dev/null || true
|
git config --global --unset-all "$key" 2>/dev/null || true
|
||||||
git config --global --unset-all "$use_path_key" 2>/dev/null || true
|
git config --global --unset-all "$use_path_key" 2>/dev/null || true
|
||||||
|
|
||||||
# Username is derived from the OAuth token at runtime by the helper; we
|
# Username is derived from the OAuth token at runtime by the helper; do
|
||||||
# deliberately do NOT set credential.<URL>.username here, to avoid
|
# deliberately do NOT set credential.<URL>.username here, to avoid
|
||||||
# pinning an old login. The helper emits username=<stored> on every
|
# pinning an old login. The helper emits username=<stored> on every
|
||||||
# `get`, which git respects.
|
# `get`, which git respects.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ orchestrator="$workspace_root/$repo_name"
|
|||||||
|
|
||||||
if [ ! -d "$orchestrator/.git" ]; then
|
if [ ! -d "$orchestrator/.git" ]; then
|
||||||
err "orchestrator checkout not found at $orchestrator"
|
err "orchestrator checkout not found at $orchestrator"
|
||||||
note "run 'just clone-orchestrator' (or 'just setup') first."
|
note "required command: just clone-orchestrator"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -119,9 +119,9 @@ if [ "$MODE" != "run" ]; then
|
|||||||
if [ "${#forward_args[@]}" -gt 0 ]; then
|
if [ "${#forward_args[@]}" -gt 0 ]; then
|
||||||
fwd_suffix=" ${forward_args[*]}"
|
fwd_suffix=" ${forward_args[*]}"
|
||||||
fi
|
fi
|
||||||
note "Execute via:"
|
note "Command:"
|
||||||
note " just run-next-steps${fwd_suffix}"
|
note " just run-next-steps${fwd_suffix}"
|
||||||
note "Execute inside the orchestrator:"
|
note "Orchestrator command:"
|
||||||
note " cd $orchestrator && just contributor-setup${fwd_suffix}"
|
note " cd $orchestrator && just contributor-setup${fwd_suffix}"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ fi
|
|||||||
load_env
|
load_env
|
||||||
|
|
||||||
if [ -z "${FORGE_GITEA_USERNAME:-}" ]; then
|
if [ -z "${FORGE_GITEA_USERNAME:-}" ]; then
|
||||||
note "FORGE_GITEA_USERNAME is blank. Enter your Gitea username exactly as it appears in your profile URL."
|
note "FORGE_GITEA_USERNAME is blank. Enter the Gitea username from the profile URL."
|
||||||
new_user="$(prompt_line "Gitea username")"
|
new_user="$(prompt_line "Gitea username")"
|
||||||
if [ -z "$new_user" ]; then
|
if [ -z "$new_user" ]; then
|
||||||
die "no username entered: aborting. Edit .env by hand and re-run 'just setup'."
|
die "no username entered: aborting. Edit .env by hand and re-run 'just setup'."
|
||||||
@@ -180,7 +180,7 @@ if [ "$run_login" = "1" ]; then
|
|||||||
forbid any interaction. Choose one:
|
forbid any interaction. Choose one:
|
||||||
1. Drop FORGE_SETUP_YES and re-run 'just setup --headless'
|
1. Drop FORGE_SETUP_YES and re-run 'just setup --headless'
|
||||||
(setup will print the authorisation URL and wait
|
(setup will print the authorisation URL and wait
|
||||||
while you paste it into a browser).
|
while the URL is pasted into a browser).
|
||||||
2. Run 'just login' once on a machine with a browser
|
2. Run 'just login' once on a machine with a browser
|
||||||
to populate the stored refresh token, then re-run
|
to populate the stored refresh token, then re-run
|
||||||
'just setup --headless' from the headless host.
|
'just setup --headless' from the headless host.
|
||||||
@@ -202,7 +202,7 @@ GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=Never \
|
|||||||
VSCODE_GIT_ASKPASS_EXTRA_ARGS="" VSCODE_GIT_IPC_HANDLE="" \
|
VSCODE_GIT_ASKPASS_EXTRA_ARGS="" VSCODE_GIT_IPC_HANDLE="" \
|
||||||
DISPLAY="" WAYLAND_DISPLAY="" \
|
DISPLAY="" WAYLAND_DISPLAY="" \
|
||||||
git ls-remote "$FORGE_ORCHESTRATOR_REPO_URL" HEAD >/dev/null 2>&1 \
|
git ls-remote "$FORGE_ORCHESTRATOR_REPO_URL" HEAD >/dev/null 2>&1 \
|
||||||
|| die "git ls-remote failed. Your account may not yet be in the org, or FORGE_ORCHESTRATOR_REPO_URL is wrong."
|
|| die "git ls-remote failed. Account membership may be missing, or FORGE_ORCHESTRATOR_REPO_URL is wrong."
|
||||||
ok "silent clone access confirmed"
|
ok "silent clone access confirmed"
|
||||||
|
|
||||||
step "6/7 cloning the orchestrator (just clone-orchestrator)"
|
step "6/7 cloning the orchestrator (just clone-orchestrator)"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|||||||
|
|
||||||
load_env
|
load_env
|
||||||
|
|
||||||
# 1. Log out at the OAuth layer (clears our managed fields from
|
# 1. Log out at the OAuth layer (clears repo-managed fields from
|
||||||
# client-auth.json and keeps any fields owned by the orchestrator
|
# client-auth.json and keeps any fields owned by the orchestrator
|
||||||
# gateway untouched).
|
# gateway untouched).
|
||||||
if [ -f "$here/forge_auth.py" ]; then
|
if [ -f "$here/forge_auth.py" ]; then
|
||||||
@@ -37,4 +37,4 @@ done
|
|||||||
|
|
||||||
info 'uninstall complete.'
|
info 'uninstall complete.'
|
||||||
info "note: $FORGE_AUTH_FILE is left in place if the orchestrator wrote it;"
|
info "note: $FORGE_AUTH_FILE is left in place if the orchestrator wrote it;"
|
||||||
info ' delete it manually if you want a completely clean state.'
|
info ' delete it manually for a fully clean state.'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ bash tests/test_doctor.sh
|
|||||||
|
|
||||||
## Inventory
|
## Inventory
|
||||||
|
|
||||||
- **`test_forge_auth.py`** — `scripts/forge_auth.py` unit tests: PKCE
|
- **`test_forge_auth.py`**: `scripts/forge_auth.py` unit tests: PKCE
|
||||||
pair generation, HMAC state signing + CSRF rejection,
|
pair generation, HMAC state signing + CSRF rejection,
|
||||||
`ForgeAuthConfig.from_env` validation (loopback-only redirect,
|
`ForgeAuthConfig.from_env` validation (loopback-only redirect,
|
||||||
missing port, missing env vars, `FORGE_GITEA_USERNAME`
|
missing port, missing env vars, `FORGE_GITEA_USERNAME`
|
||||||
@@ -22,19 +22,19 @@ bash tests/test_doctor.sh
|
|||||||
read/write/merge/`has_live_gitea_token`, `auth_store_path`
|
read/write/merge/`has_live_gitea_token`, `auth_store_path`
|
||||||
precedence, `run_logout`, `main()` dispatcher.
|
precedence, `run_logout`, `main()` dispatcher.
|
||||||
|
|
||||||
- **`test_git_credential_forge.py`** — `scripts/git-credential-forge.py`
|
- **`test_git_credential_forge.py`**: `scripts/git-credential-forge.py`
|
||||||
unit tests: credential protocol I/O, host/scheme/port matching,
|
unit tests: credential protocol I/O, host/scheme/port matching,
|
||||||
live-token fast-path, pass-through for missing store or non-matching
|
live-token fast-path, pass-through for missing store or non-matching
|
||||||
host, expired-token refresh, refresh-failure handling, `store`/`erase`
|
host, expired-token refresh, refresh-failure handling, `store`/`erase`
|
||||||
no-ops, `main()` dispatcher.
|
no-ops, `main()` dispatcher.
|
||||||
|
|
||||||
- **`test_forge_auth_integration.py`** — end-to-end Python integration
|
- **`test_forge_auth_integration.py`**: end-to-end Python integration
|
||||||
tests against `tests/mock_oidc_server.py`: full PKCE flow,
|
tests against `tests/mock_oidc_server.py`: full PKCE flow,
|
||||||
gateway-required schema on disk, idempotent re-login, refresh token
|
gateway-required schema on disk, idempotent re-login, refresh token
|
||||||
rotation with server-side revocation, logout preserving
|
rotation with server-side revocation, logout preserving
|
||||||
gateway-bearer fields.
|
gateway-bearer fields.
|
||||||
|
|
||||||
- **`test_forge_auth_integration.sh`** — shell end-to-end: drives
|
- **`test_forge_auth_integration.sh`**: shell end-to-end: drives
|
||||||
`forge_auth.py login` against the mock server, installs the
|
`forge_auth.py login` against the mock server, installs the
|
||||||
credential helper into a sandboxed `$HOME`, and exercises
|
credential helper into a sandboxed `$HOME`, and exercises
|
||||||
`git credential fill`. Covers URL matching, `github.com`
|
`git credential fill`. Covers URL matching, `github.com`
|
||||||
@@ -43,17 +43,17 @@ bash tests/test_doctor.sh
|
|||||||
logout URL surfaced, authorise URL carries `prompt=login` +
|
logout URL surfaced, authorise URL carries `prompt=login` +
|
||||||
`login_hint`).
|
`login_hint`).
|
||||||
|
|
||||||
- **`test_setup_args.sh`** — `scripts/setup.sh` coverage: argument
|
- **`test_setup_args.sh`**: `scripts/setup.sh` coverage: argument
|
||||||
parsing, `--help`, `--headless` wiring to `forge_login.sh
|
parsing, `--help`, `--headless` wiring to `forge_login.sh
|
||||||
--no-browser`, the `--headless + FORGE_SETUP_YES=1` hang guard,
|
--no-browser`, the `--headless + FORGE_SETUP_YES=1` hang guard,
|
||||||
live-token reuse, silent-refresh rescue, `prompt_choice` non-tty
|
live-token reuse, silent-refresh rescue, `prompt_choice` non-tty
|
||||||
stdout isolation.
|
stdout isolation.
|
||||||
|
|
||||||
- **`test_doctor.sh`** — `scripts/doctor.sh`: miss-path under a
|
- **`test_doctor.sh`**: `scripts/doctor.sh`: miss-path under a
|
||||||
sandboxed PATH, asserts every `[MISS]` line is followed by a `fix:`
|
sandboxed PATH, asserts every `[MISS]` line is followed by a `fix:`
|
||||||
line.
|
line.
|
||||||
|
|
||||||
- **`mock_oidc_server.py`** — test fixture implementing
|
- **`mock_oidc_server.py`**: test fixture implementing
|
||||||
`/.well-known/openid-configuration`, `/login/oauth/authorize`,
|
`/.well-known/openid-configuration`, `/login/oauth/authorize`,
|
||||||
`/login/oauth/access_token`, `/login/oauth/userinfo`. PKCE
|
`/login/oauth/access_token`, `/login/oauth/userinfo`. PKCE
|
||||||
verification on `authorization_code`; rotation + revocation on
|
verification on `authorization_code`; rotation + revocation on
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ out="$(mktemp)"
|
|||||||
trap 'rm -f "$out"' EXIT
|
trap 'rm -f "$out"' EXIT
|
||||||
|
|
||||||
# Strip everything that could satisfy the checks we want to fail.
|
# Strip everything that could satisfy the checks we want to fail.
|
||||||
# /usr/bin/python3 is 3.10.x on Ubuntu 22.04; that's fine: we want to
|
# /usr/bin/python3 is 3.10.x on Ubuntu 22.04; this case exercises the
|
||||||
# prove the python>=3.11 miss branch renders its fix+alt lines.
|
# python>=3.11 miss branch and its fix+alt lines.
|
||||||
if env -i HOME="$HOME" PATH="/usr/bin:/bin" bash "$repo/scripts/doctor.sh" >"$out" 2>&1; then
|
if env -i HOME="$HOME" PATH="/usr/bin:/bin" bash "$repo/scripts/doctor.sh" >"$out" 2>&1; then
|
||||||
echo "FAIL: doctor.sh exited 0 despite missing prerequisites"
|
echo "FAIL: doctor.sh exited 0 despite missing prerequisites"
|
||||||
cat "$out"
|
cat "$out"
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ class AuthFileTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_merge_login_preserves_gateway_bearer(self) -> None:
|
def test_merge_login_preserves_gateway_bearer(self) -> None:
|
||||||
# Simulates the case where the orchestrator already ran
|
# Simulates the case where the orchestrator already ran
|
||||||
# `auth login` and populated the gateway bearer. We must not
|
# `auth login` and populated the gateway bearer. The code must not
|
||||||
# overwrite those fields.
|
# overwrite those fields.
|
||||||
f = fa.AuthFile(raw={
|
f = fa.AuthFile(raw={
|
||||||
"username": "old-alice",
|
"username": "old-alice",
|
||||||
@@ -396,8 +396,8 @@ class AuthFileTests(unittest.TestCase):
|
|||||||
self.assertEqual(roundtrip["username"], "u")
|
self.assertEqual(roundtrip["username"], "u")
|
||||||
|
|
||||||
def test_write_preserves_unknown_keys(self) -> None:
|
def test_write_preserves_unknown_keys(self) -> None:
|
||||||
# Forward-compat: the gateway might add new fields we don't
|
# Forward-compat: the gateway might add new fields unknown to the
|
||||||
# know about. Writing must preserve them verbatim.
|
# current schema. Writing must preserve them verbatim.
|
||||||
raw = {"username": "u", "future_field": {"x": 1}, "access_token": "A"}
|
raw = {"username": "u", "future_field": {"x": 1}, "access_token": "A"}
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
p = Path(d) / "a.json"
|
p = Path(d) / "a.json"
|
||||||
@@ -644,7 +644,7 @@ class BuildAuthorizeErrorTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_different_scope_without_base_url_uses_placeholder(self) -> None:
|
def test_different_scope_without_base_url_uses_placeholder(self) -> None:
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"<your-gitea-url>/user/settings/applications",
|
"<gitea-base-url>/user/settings/applications",
|
||||||
str(self._exc_different_scope(gitea_base_url="")),
|
str(self._exc_different_scope(gitea_base_url="")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Covers the full PKCE login flow (authorize → callback → token
|
|||||||
exchange → userinfo → persist), transparent refresh, logout, and
|
exchange → userinfo → persist), transparent refresh, logout, and
|
||||||
the idempotent "already authenticated" short-circuit.
|
the idempotent "already authenticated" short-circuit.
|
||||||
|
|
||||||
No real network calls. No browser required: we simulate the
|
No real network calls. No browser required: the test simulates the
|
||||||
browser by doing an HTTP GET to the authorize endpoint; the mock
|
browser by doing an HTTP GET to the authorize endpoint; the mock
|
||||||
server 302-redirects to the loopback callback, which
|
server 302-redirects to the loopback callback, which
|
||||||
`forge_auth.run_login` is already listening on.
|
`forge_auth.run_login` is already listening on.
|
||||||
@@ -44,14 +44,14 @@ def _free_loopback_port() -> int:
|
|||||||
class _MockBrowser:
|
class _MockBrowser:
|
||||||
"""Drive the authorize endpoint on a worker thread.
|
"""Drive the authorize endpoint on a worker thread.
|
||||||
|
|
||||||
We wait a fraction of a second for `run_login` to bind its
|
The worker waits a fraction of a second for `run_login` to bind its
|
||||||
loopback callback server, then GET the authorize URL. The mock
|
loopback callback server, then GET the authorize URL. The mock
|
||||||
server redirects us to the callback; following the redirect
|
server redirects to the callback; following the redirect
|
||||||
causes `run_login`'s callback handler to fire, and the auth flow
|
causes `run_login`'s callback handler to fire, and the auth flow
|
||||||
completes.
|
completes.
|
||||||
|
|
||||||
urllib's default opener follows redirects automatically, which is
|
urllib's default opener follows redirects automatically, which is
|
||||||
exactly what we want here: one GET, one automatic redirect, done.
|
the required behavior here: one GET, one automatic redirect, done.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, authorize_url: str, delay_seconds: float = 0.2) -> None:
|
def __init__(self, authorize_url: str, delay_seconds: float = 0.2) -> None:
|
||||||
@@ -111,14 +111,14 @@ class ForgeAuthIntegrationTests(unittest.TestCase):
|
|||||||
# Helpers
|
# Helpers
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
def _login(self) -> fa.AuthFile:
|
def _login(self) -> fa.AuthFile:
|
||||||
"""Run run_login() with an auto-browser that does the GET for us."""
|
"""Run run_login() with an auto-browser issuing the authorize GET."""
|
||||||
with mock.patch.dict(os.environ, self.env, clear=True):
|
with mock.patch.dict(os.environ, self.env, clear=True):
|
||||||
config = fa.ForgeAuthConfig.from_env()
|
config = fa.ForgeAuthConfig.from_env()
|
||||||
|
|
||||||
# We need to start the mock "browser" AFTER run_login
|
# The mock browser starts AFTER run_login
|
||||||
# prints the authorize URL but BEFORE it blocks on the
|
# prints the authorize URL but BEFORE it blocks on the
|
||||||
# loopback server. Since run_login prints then blocks
|
# loopback server. Since run_login prints then blocks
|
||||||
# synchronously, we can intercept webbrowser.open to
|
# synchronously, the code intercepts webbrowser.open to
|
||||||
# kick off the GET at exactly the right moment.
|
# kick off the GET at exactly the right moment.
|
||||||
browser_holder: dict[str, _MockBrowser] = {}
|
browser_holder: dict[str, _MockBrowser] = {}
|
||||||
|
|
||||||
@@ -240,8 +240,8 @@ class ForgeAuthIntegrationTests(unittest.TestCase):
|
|||||||
def test_callback_state_csrf_mismatch_raises(self) -> None:
|
def test_callback_state_csrf_mismatch_raises(self) -> None:
|
||||||
"""A tampered state on the callback must raise.
|
"""A tampered state on the callback must raise.
|
||||||
|
|
||||||
We cannot easily tamper with the real PKCE flow end-to-end,
|
The real PKCE flow is not easily tampered end-to-end here,
|
||||||
so we exercise verify_state directly: the `run_login` path
|
so the test exercises verify_state directly: the `run_login` path
|
||||||
wires it straight through.
|
wires it straight through.
|
||||||
"""
|
"""
|
||||||
key = b"\x01" * 32
|
key = b"\x01" * 32
|
||||||
|
|||||||
Reference in New Issue
Block a user