git-HTTP: accept job token + PAT for private-repo clone
1fdbefa · gmorell · 2026-06-26 00:55
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud_web/plugs/git_smart_http.ex
+35
−6
@@ -26,6 +26,7 @@ defmodule GitGudWeb.Plugs.GitSmartHttp do
| 26 | 26 | alias GitGud.Accounts |
| 27 | 27 | alias GitGud.Repositories |
| 28 | 28 | alias GitGud.Repositories.Repository |
| 29 | + alias GitGud.Workflows.RunnerJwt | |
| 29 | 30 | |
| 30 | 31 | @service_path_info ["info", "refs"] |
| 31 | 32 | @upload_pack "git-upload-pack" |
@@ -187,23 +188,51 @@ defmodule GitGudWeb.Plugs.GitSmartHttp do
| 187 | 188 | defp authorize(conn, %Repository{visibility: "public"} = _repo, @upload_pack), do: {:ok, conn} |
| 188 | 189 | |
| 189 | 190 | defp authorize(conn, %Repository{} = repo, service) do |
| 190 | − case basic_auth_user(conn) do | |
| 191 | − {:ok, user} -> | |
| 191 | + case authenticate(conn) do | |
| 192 | + {:user, user} -> | |
| 192 | 193 | if can?(user, repo, service), |
| 193 | 194 | do: {:ok, assign(conn, :git_pusher_id, user.id)}, |
| 194 | 195 | else: {:error, :forbidden} |
| 195 | 196 | |
| 197 | + # Per-job runner token (what `actions/checkout` sends as | |
| 198 | + # `x-access-token:<github.token>`). Read-only, and only its own | |
| 199 | + # repo — enough to check the source out, nothing more. | |
| 200 | + {:job, pid} -> | |
| 201 | + if service == @upload_pack and pid == repo.id, | |
| 202 | + do: {:ok, conn}, | |
| 203 | + else: {:error, :forbidden} | |
| 204 | + | |
| 196 | 205 | :error -> |
| 197 | 206 | {:error, :unauthorized} |
| 198 | 207 | end |
| 199 | 208 | end |
| 200 | 209 | |
| 201 | − defp basic_auth_user(conn) do | |
| 210 | + # Resolve Basic-auth credentials to a principal: | |
| 211 | + # {:user, %User{}} — forge password OR a Personal Access Token | |
| 212 | + # {:job, repo_id} — a per-job RunnerJwt | |
| 213 | + # :error | |
| 214 | + # The token is carried in the password field (the username is whatever | |
| 215 | + # the client sent — e.g. `x-access-token` — and is ignored for tokens). | |
| 216 | + defp authenticate(conn) do | |
| 202 | 217 | with ["Basic " <> b64] <- get_req_header(conn, "authorization"), |
| 203 | 218 | {:ok, decoded} <- Base.decode64(b64), |
| 204 | − [email, password] <- String.split(decoded, ":", parts: 2), | |
| 205 | − %_{} = user <- Accounts.get_user_by_email_and_password(email, password) do | |
| 206 | − {:ok, user} | |
| 219 | + [username, secret] <- String.split(decoded, ":", parts: 2) do | |
| 220 | + case RunnerJwt.verify(secret) do | |
| 221 | + {:ok, %{"pid" => pid}} -> | |
| 222 | + {:job, pid} | |
| 223 | + | |
| 224 | + _ -> | |
| 225 | + case Accounts.verify_personal_access_token(secret) do | |
| 226 | + {:ok, user, _scopes} -> | |
| 227 | + {:user, user} | |
| 228 | + | |
| 229 | + :error -> | |
| 230 | + case Accounts.get_user_by_email_and_password(username, secret) do | |
| 231 | + %_{} = user -> {:user, user} | |
| 232 | + _ -> :error | |
| 233 | + end | |
| 234 | + end | |
| 235 | + end | |
| 207 | 236 | else |
| 208 | 237 | _ -> :error |
| 209 | 238 | end |
Parents: 0fe752f