gitea: fix actions visibility
This commit is contained in:
@@ -50,14 +50,14 @@
|
||||
};
|
||||
|
||||
# Hide repo Actions/workflow details from anonymous visitors. Gitea's own
|
||||
# REQUIRE_SIGNIN_VIEW=expensive mode does not cover /{user}/{repo}/actions,
|
||||
# so we gate the path at Caddy: forward_auth probes Gitea's /api/v1/user
|
||||
# with the incoming request's Cookie/Authorization headers. A logged-in
|
||||
# session answers 200 and the original request falls through to the
|
||||
# reverse_proxy from mkCaddyReverseProxy; a 401 is turned into a redirect
|
||||
# to the login page so the browser shows the login form instead of the
|
||||
# workflow list. Workflow status badges stay public so README links keep
|
||||
# rendering.
|
||||
# REQUIRE_SIGNIN_VIEW=expensive does not cover /{user}/{repo}/actions, and
|
||||
# the API auth chain (routers/api/v1/api.go buildAuthGroup) deliberately
|
||||
# omits `auth_service.Session`, so an /api/v1/user probe would 401 even
|
||||
# for logged-in browser sessions. We gate at Caddy instead: forward_auth
|
||||
# probes a lightweight *web-UI* endpoint that does accept session cookies,
|
||||
# and Gitea's own reqSignIn middleware answers 303 to /user/login for
|
||||
# anonymous callers which we rewrite to preserve the original URL.
|
||||
# Workflow status badges stay public so README links keep rendering.
|
||||
services.caddy.virtualHosts.${service_configs.gitea.domain}.extraConfig = ''
|
||||
@repoActionsNotBadge {
|
||||
path_regexp ^/[^/]+/[^/]+/actions(/.*)?$
|
||||
@@ -65,9 +65,9 @@
|
||||
}
|
||||
handle @repoActionsNotBadge {
|
||||
forward_auth :${toString service_configs.ports.private.gitea.port} {
|
||||
uri /api/v1/user
|
||||
uri /user/stopwatches
|
||||
|
||||
@unauthorized status 401
|
||||
@unauthorized status 302 303
|
||||
handle_response @unauthorized {
|
||||
redir * /user/login?redirect_to={uri} 302
|
||||
}
|
||||
|
||||
@@ -67,9 +67,14 @@ pkgs.testers.runNixOSTest {
|
||||
# `service_configs.gitea.domain` string, which here is the full URL
|
||||
# `http://server`. Override to valid bare values so Gitea doesn't
|
||||
# get a malformed ROOT_URL like `https://http://server`.
|
||||
services.gitea.settings.server = {
|
||||
DOMAIN = lib.mkForce "server";
|
||||
ROOT_URL = lib.mkForce "http://server/";
|
||||
services.gitea.settings = {
|
||||
server = {
|
||||
DOMAIN = lib.mkForce "server";
|
||||
ROOT_URL = lib.mkForce "http://server/";
|
||||
};
|
||||
# Tests talk HTTP, so drop the Secure flag — otherwise curl's cookie
|
||||
# jar holds the session cookie but never sends it back.
|
||||
session.COOKIE_SECURE = lib.mkForce false;
|
||||
};
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
@@ -103,6 +108,8 @@ pkgs.testers.runNixOSTest {
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import re
|
||||
|
||||
start_all()
|
||||
server.wait_for_unit("postgresql.service")
|
||||
server.wait_for_unit("gitea.service")
|
||||
@@ -110,7 +117,6 @@ pkgs.testers.runNixOSTest {
|
||||
server.wait_for_open_port(3000)
|
||||
server.wait_for_open_port(80)
|
||||
|
||||
# Admin user — used to exercise the authenticated path via Basic Auth.
|
||||
server.succeed(
|
||||
"su -l gitea -s /bin/sh -c '${pkgs.gitea}/bin/gitea admin user create "
|
||||
"--username testuser --password testpassword "
|
||||
@@ -118,13 +124,44 @@ pkgs.testers.runNixOSTest {
|
||||
"--work-path /var/lib/gitea'"
|
||||
)
|
||||
|
||||
def curl(args):
|
||||
def curl(args, cookies=None):
|
||||
cookie_args = f"-b {cookies} " if cookies else ""
|
||||
cmd = (
|
||||
"curl -4 -s -o /dev/null "
|
||||
"-w '%{http_code}|%{redirect_url}' " + args
|
||||
f"-w '%{{http_code}}|%{{redirect_url}}' {cookie_args}{args}"
|
||||
)
|
||||
return client.succeed(cmd).strip()
|
||||
|
||||
def login():
|
||||
# Gitea's POST /user/login requires a _csrf token and expects the
|
||||
# matching session cookie already set. Fetch the login form first
|
||||
# to harvest both, then submit credentials with the same cookie jar.
|
||||
client.succeed("rm -f /tmp/cookies.txt")
|
||||
html = client.succeed(
|
||||
"curl -4 -s -c /tmp/cookies.txt http://server/user/login"
|
||||
)
|
||||
match = re.search(r'name="_csrf"\s+value="([^"]+)"', html)
|
||||
assert match, f"CSRF token not found in login form: {html[:500]!r}"
|
||||
csrf = match.group(1)
|
||||
# -L so we follow the post-login redirect; the session cookie is
|
||||
# rewritten by Gitea on successful login to carry uid.
|
||||
client.succeed(
|
||||
"curl -4 -s -L -o /dev/null "
|
||||
"-b /tmp/cookies.txt -c /tmp/cookies.txt "
|
||||
f"--data-urlencode '_csrf={csrf}' "
|
||||
"--data-urlencode 'user_name=testuser' "
|
||||
"--data-urlencode 'password=testpassword' "
|
||||
"http://server/user/login"
|
||||
)
|
||||
# Sanity-check the session by hitting the gated probe directly —
|
||||
# the post-login cookie jar MUST drive /user/stopwatches to 200.
|
||||
probe = client.succeed(
|
||||
"curl -4 -s -o /dev/null -w '%{http_code}' "
|
||||
"-b /tmp/cookies.txt http://server/user/stopwatches"
|
||||
).strip()
|
||||
assert probe == "200", f"session auth probe expected 200, got {probe!r}"
|
||||
return "/tmp/cookies.txt"
|
||||
|
||||
with subtest("Anonymous /{user}/{repo}/actions redirects to login"):
|
||||
result = curl("http://server/foo/bar/actions")
|
||||
code, _, redir = result.partition("|")
|
||||
@@ -132,6 +169,9 @@ pkgs.testers.runNixOSTest {
|
||||
assert code == "302", f"expected 302, got {code!r} (full: {result!r})"
|
||||
assert "/user/login" in redir, f"expected login redirect, got {redir!r}"
|
||||
assert "redirect_to=" in redir, f"expected redirect_to param, got {redir!r}"
|
||||
assert "/foo/bar/actions" in redir, (
|
||||
f"expected original URL preserved in redirect_to, got {redir!r}"
|
||||
)
|
||||
|
||||
with subtest("Anonymous deep /actions paths also redirect"):
|
||||
for path in ["/foo/bar/actions/", "/foo/bar/actions/runs/1", "/foo/bar/actions/workflows/build.yaml"]:
|
||||
@@ -142,8 +182,6 @@ pkgs.testers.runNixOSTest {
|
||||
assert "/user/login" in redir, f"{path}: expected login redirect, got {redir!r}"
|
||||
|
||||
with subtest("Anonymous workflow badge stays public"):
|
||||
# Repo doesn't exist → Gitea answers 404, but Caddy does NOT intercept
|
||||
# with a login redirect so README badges keep rendering.
|
||||
result = curl("http://server/foo/bar/actions/workflows/ci.yaml/badge.svg")
|
||||
code, _, redir = result.partition("|")
|
||||
print(f"anon badge -> {result!r}")
|
||||
@@ -151,17 +189,18 @@ pkgs.testers.runNixOSTest {
|
||||
f"badge path should not redirect to login, got {result!r}"
|
||||
)
|
||||
|
||||
with subtest("Authenticated /{user}/{repo}/actions does not redirect to login"):
|
||||
cookies = login()
|
||||
|
||||
with subtest("Session-authenticated /{user}/{repo}/actions reaches Gitea"):
|
||||
result = curl(
|
||||
"-u testuser:testpassword "
|
||||
"http://server/testuser/nonexistent/actions"
|
||||
"http://server/testuser/nonexistent/actions", cookies=cookies
|
||||
)
|
||||
code, _, redir = result.partition("|")
|
||||
print(f"auth /testuser/nonexistent/actions -> {result!r}")
|
||||
# Gitea will 404 the missing repo — the key assertion is that the
|
||||
# Caddy gate let the request through instead of redirecting to login.
|
||||
# Gitea returns 404 for the missing repo — the key assertion is that
|
||||
# Caddy's gate forwarded the request instead of redirecting to login.
|
||||
assert not (code == "302" and "/user/login" in redir), (
|
||||
f"authenticated actions request was intercepted by login gate: {result!r}"
|
||||
f"session-authed actions request was intercepted by login gate: {result!r}"
|
||||
)
|
||||
|
||||
with subtest("Anonymous /explore/repos is served without gating"):
|
||||
|
||||
Reference in New Issue
Block a user