git-HTTP: accept job token + PAT for private-repo clone

1fdbefa · gmorell · 2026-06-26 00:55

1 files +35 -6
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