fix devpi gateway auth
This commit is contained in:
@@ -12,11 +12,12 @@ On every ``get`` the helper:
|
|||||||
1. Reads the auth file (same path the gateway uses:
|
1. Reads the auth file (same path the gateway uses:
|
||||||
``~/.forge-stack-devpi-gateway-gitea/client-auth.json`` by default,
|
``~/.forge-stack-devpi-gateway-gitea/client-auth.json`` by default,
|
||||||
or whatever ``FSDGG_AUTH_STORE_PATH`` / ``FSDGG_RUNTIME_DIR`` point
|
or whatever ``FSDGG_AUTH_STORE_PATH`` / ``FSDGG_RUNTIME_DIR`` point
|
||||||
at). If the file is missing, the helper passes git's input through
|
at). If the file is missing, the helper emits no output so the next
|
||||||
unchanged so the normal prompt chain continues.
|
helper in Git's chain can answer without receiving echoed fields from
|
||||||
|
this helper.
|
||||||
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, emits no
|
||||||
through. This prevents OAuth token disclosure to unrelated
|
output. 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 +30,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 the helper exits **non-zero** rather than silently
|
* On OAuth refresh failure the helper emits no credentials rather than
|
||||||
returning stale credentials.
|
returning stale credentials.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -82,6 +83,10 @@ def emit(fields: dict[str, str]) -> None:
|
|||||||
sys.stdout.write(f"{key}={value}\n")
|
sys.stdout.write(f"{key}={value}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def emit_no_credentials() -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# Host matching
|
# Host matching
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
@@ -150,10 +155,10 @@ 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: helper scope is unknown
|
emit_no_credentials()
|
||||||
return 0
|
return 0
|
||||||
if not _request_matches(fields, configured):
|
if not _request_matches(fields, configured):
|
||||||
emit(fields) # pass-through: request targets a different host
|
emit_no_credentials()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
state = forge_auth.AuthFile.read(forge_auth.auth_store_path())
|
state = forge_auth.AuthFile.read(forge_auth.auth_store_path())
|
||||||
@@ -172,7 +177,7 @@ def cmd_get(fields: dict[str, str]) -> int:
|
|||||||
"FSDGG_CLI_CLIENT_ID are not set in the environment; "
|
"FSDGG_CLI_CLIENT_ID are not set in the environment; "
|
||||||
"cannot refresh. Run 'just login' to re-authenticate."
|
"cannot refresh. Run 'just login' to re-authenticate."
|
||||||
)
|
)
|
||||||
emit(fields)
|
emit_no_credentials()
|
||||||
return 0
|
return 0
|
||||||
try:
|
try:
|
||||||
refreshed = forge_auth.run_refresh(config, must_refresh=True)
|
refreshed = forge_auth.run_refresh(config, must_refresh=True)
|
||||||
@@ -181,7 +186,7 @@ def cmd_get(fields: dict[str, str]) -> int:
|
|||||||
f"token refresh failed: {exc}. "
|
f"token refresh failed: {exc}. "
|
||||||
f"Run 'just login' to re-authenticate."
|
f"Run 'just login' to re-authenticate."
|
||||||
)
|
)
|
||||||
emit(fields)
|
emit_no_credentials()
|
||||||
return 0
|
return 0
|
||||||
_emit_credentials(fields, refreshed)
|
_emit_credentials(fields, refreshed)
|
||||||
return 0
|
return 0
|
||||||
@@ -190,7 +195,7 @@ def cmd_get(fields: dict[str, str]) -> int:
|
|||||||
def _emit_credentials(fields: dict[str, str], state: forge_auth.AuthFile) -> None:
|
def _emit_credentials(fields: dict[str, str], state: forge_auth.AuthFile) -> None:
|
||||||
token = state.gitea_access_token
|
token = state.gitea_access_token
|
||||||
if not token:
|
if not token:
|
||||||
emit(fields)
|
emit_no_credentials()
|
||||||
return
|
return
|
||||||
out = dict(fields)
|
out = dict(fields)
|
||||||
out["username"] = state.username or fields.get("username") or "oauth"
|
out["username"] = state.username or fields.get("username") or "oauth"
|
||||||
|
|||||||
@@ -177,16 +177,16 @@ class CmdGetTests(unittest.TestCase):
|
|||||||
self.assertEqual(parsed["password"], "LIVETOKEN")
|
self.assertEqual(parsed["password"], "LIVETOKEN")
|
||||||
self.assertEqual(parsed["host"], "g.example:8443")
|
self.assertEqual(parsed["host"], "g.example:8443")
|
||||||
|
|
||||||
def test_no_store_passes_through(self) -> None:
|
def test_no_store_emits_no_credentials(self) -> None:
|
||||||
rc, out, _ = self._run(
|
rc, out, _ = self._run(
|
||||||
store_payload=None,
|
store_payload=None,
|
||||||
stdin_text="protocol=https\nhost=g.example:8443\n\n",
|
stdin_text="protocol=https\nhost=g.example:8443\n\n",
|
||||||
)
|
)
|
||||||
self.assertEqual(rc, 0)
|
self.assertEqual(rc, 0)
|
||||||
|
self.assertEqual(out, "")
|
||||||
self.assertNotIn("password=", out)
|
self.assertNotIn("password=", out)
|
||||||
self.assertIn("host=g.example:8443", out)
|
|
||||||
|
|
||||||
def test_non_matching_host_passes_through(self) -> None:
|
def test_non_matching_host_emits_no_credentials(self) -> None:
|
||||||
payload = {
|
payload = {
|
||||||
"username": "alice",
|
"username": "alice",
|
||||||
"gitea_access_token": "LIVETOKEN",
|
"gitea_access_token": "LIVETOKEN",
|
||||||
@@ -198,10 +198,10 @@ class CmdGetTests(unittest.TestCase):
|
|||||||
stdin_text="protocol=https\nhost=github.com\n\n",
|
stdin_text="protocol=https\nhost=github.com\n\n",
|
||||||
)
|
)
|
||||||
self.assertEqual(rc, 0)
|
self.assertEqual(rc, 0)
|
||||||
|
self.assertEqual(out, "")
|
||||||
self.assertNotIn("password=", out)
|
self.assertNotIn("password=", out)
|
||||||
self.assertIn("host=github.com", out)
|
|
||||||
|
|
||||||
def test_match_but_no_token_passes_through(self) -> None:
|
def test_match_but_no_token_emits_no_credentials(self) -> None:
|
||||||
payload = {
|
payload = {
|
||||||
"username": "alice",
|
"username": "alice",
|
||||||
"gitea_access_token": "",
|
"gitea_access_token": "",
|
||||||
@@ -212,6 +212,7 @@ class CmdGetTests(unittest.TestCase):
|
|||||||
stdin_text="protocol=https\nhost=g.example:8443\n\n",
|
stdin_text="protocol=https\nhost=g.example:8443\n\n",
|
||||||
)
|
)
|
||||||
self.assertEqual(rc, 0)
|
self.assertEqual(rc, 0)
|
||||||
|
self.assertEqual(out, "")
|
||||||
self.assertNotIn("password=", out)
|
self.assertNotIn("password=", out)
|
||||||
|
|
||||||
# --- expired token + refresh -------------------------------------
|
# --- expired token + refresh -------------------------------------
|
||||||
@@ -263,6 +264,7 @@ class CmdGetTests(unittest.TestCase):
|
|||||||
stdin_text="protocol=https\nhost=g.example:8443\n\n",
|
stdin_text="protocol=https\nhost=g.example:8443\n\n",
|
||||||
)
|
)
|
||||||
self.assertEqual(rc, 0)
|
self.assertEqual(rc, 0)
|
||||||
|
self.assertEqual(out, "")
|
||||||
self.assertNotIn("password=", out)
|
self.assertNotIn("password=", out)
|
||||||
self.assertIn("token refresh failed", err)
|
self.assertIn("token refresh failed", err)
|
||||||
self.assertIn("just login", err)
|
self.assertIn("just login", err)
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ assert() {
|
|||||||
|
|
||||||
assert 'setup.sh parses as valid bash' bash -n "$root/scripts/setup.sh"
|
assert 'setup.sh parses as valid bash' bash -n "$root/scripts/setup.sh"
|
||||||
|
|
||||||
help_out="$(bash "$root/scripts/setup.sh" --help 2>&1)"
|
help_file="$(mktemp)"
|
||||||
|
bash "$root/scripts/setup.sh" --help >"$help_file" 2>&1
|
||||||
assert '--help prints the Usage header' \
|
assert '--help prints the Usage header' \
|
||||||
bash -c "printf '%s' \"$help_out\" | grep -q '^Usage: just setup'"
|
grep -q '^Usage: just setup' "$help_file"
|
||||||
assert '--help documents --headless' \
|
assert '--help documents --headless' \
|
||||||
bash -c "printf '%s' \"$help_out\" | grep -q -- '--headless'"
|
grep -q -- '--headless' "$help_file"
|
||||||
assert '--help documents FORGE_SETUP_YES' \
|
assert '--help documents FORGE_SETUP_YES' \
|
||||||
bash -c "printf '%s' \"$help_out\" | grep -q 'FORGE_SETUP_YES'"
|
grep -q 'FORGE_SETUP_YES' "$help_file"
|
||||||
|
rm -f "$help_file"
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
bash "$root/scripts/setup.sh" --not-a-flag >/dev/null 2>"$here/.bad.err"
|
bash "$root/scripts/setup.sh" --not-a-flag >/dev/null 2>"$here/.bad.err"
|
||||||
|
|||||||
Reference in New Issue
Block a user