defmodule GitGudWeb.RateLimitsTest do
@moduledoc """
Where `GitGud.RateLimit` is applied: wrong passwords on the login
form and over git HTTPS, sign-in emails, and runner polls.
Each test comes from its own client address so the shared counters
of other tests (all from 127.0.0.1) don't leak in.
"""
use GitGudWeb.ConnCase, async: false
@moduletag :capture_log
import Ecto.Query
import Phoenix.LiveViewTest
import GitGud.AccountsFixtures
alias GitGud.RateLimit
alias GitGud.Repositories.Storage
alias Runner.V1.{FetchTaskRequest, FetchTaskResponse}
setup %{conn: conn} do
n = System.unique_integer([:positive])
ip = {198, 18, div(rem(n, 65_536), 256), rem(n, 256)}
{:ok, conn: %{conn | remote_ip: ip}, ip: ip}
end
defp login(conn, email, password) do
post(conn, ~p"/users/log-in", %{"user" => %{"email" => email, "password" => password}})
end
describe "login form" do
test "after too many wrong passwords even the right one is refused", %{conn: conn} do
user = set_password(user_fixture())
{limit, _window} = RateLimit.limits(:login_account)
for _ <- 1..limit do
conn = login(conn, user.email, "wrong password!")
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
end
conn = login(conn, user.email, valid_user_password())
refute get_session(conn, :user_token)
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "Too many wrong passwords"
end
test "an unknown address is limited the same, giving nothing away", %{conn: conn} do
{limit, _window} = RateLimit.limits(:login_account)
email = "nobody-#{System.unique_integer([:positive])}@example.com"
for _ <- 1..limit, do: login(conn, email, "wrong password!")
assert Phoenix.Flash.get(login(conn, email, "x").assigns.flash, :error) =~
"Too many wrong passwords"
end
test "a right password clears the account's count", %{conn: conn} do
user = set_password(user_fixture())
{limit, _window} = RateLimit.limits(:login_account)
for _ <- 1..(limit - 1), do: login(conn, user.email, "wrong password!")
assert get_session(login(conn, user.email, valid_user_password()), :user_token)
for _ <- 1..(limit - 1), do: login(conn, user.email, "wrong password!")
assert get_session(login(conn, user.email, valid_user_password()), :user_token)
end
test "one address guessing across accounts runs out", %{conn: conn} do
{limit, _window} = RateLimit.limits(:login_ip)
for i <- 1..limit, do: login(conn, "walk-#{i}-#{System.unique_integer()}@example.com", "x")
user = set_password(user_fixture())
conn = login(conn, user.email, valid_user_password())
refute get_session(conn, :user_token)
# Another address is unaffected.
conn =
login(%{build_conn() | remote_ip: {198, 19, 0, 1}}, user.email, valid_user_password())
assert get_session(conn, :user_token)
end
end
describe "git over HTTPS" do
@tag :git_required
test "a password guessed too often is answered 429", %{conn: conn} do
{user, repo} = GitGud.ForgeFixtures.repository_fixture(%{visibility: "private"})
user = set_password(user)
path = "/#{Storage.owner_handle(user)}/#{repo.name}.git/info/refs?service=git-upload-pack"
{limit, _window} = RateLimit.limits(:login_account)
fetch = fn password ->
conn
|> put_req_header("authorization", "Basic " <> Base.encode64("#{user.email}:#{password}"))
|> get(path)
end
assert fetch.(valid_user_password()).status == 200
for _ <- 1..limit, do: assert(fetch.("wrong password!").status == 401)
resp = fetch.(valid_user_password())
assert resp.status == 429
assert [_] = get_resp_header(resp, "retry-after")
end
end
describe "sign-in emails" do
test "past the limit for an address, no more links are made", %{conn: conn, ip: ip} do
# A LiveView finds the client from the socket's peer.
conn = Plug.Test.put_peer_data(conn, %{address: ip, port: 4000, ssl_cert: nil})
user = user_fixture()
{limit, _window} = RateLimit.limits(:email_address)
ask = fn ->
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
{:ok, _lv, html} =
form(lv, "#login_form_magic", user: %{email: user.email})
|> render_submit()
|> follow_redirect(conn, ~p"/users/log-in")
# The page says the same either way.
assert html =~ "If your email is in our system"
end
for _ <- 1..(limit + 2), do: ask.()
tokens =
GitGud.Repo.all(
from t in GitGud.Accounts.UserToken,
where: t.user_id == ^user.id and t.context == "login"
)
assert length(tokens) == limit
end
end
describe "runner polls" do
setup do
previous = Application.get_env(:git_gud, RateLimit, [])
Application.put_env(:git_gud, RateLimit, Keyword.put(previous, :runner_fetch, {2, 60}))
on_exit(fn -> Application.put_env(:git_gud, RateLimit, previous) end)
{:ok, _runner, token} =
GitGud.Workflows.Runners.register(%{
name: "poller",
version: "v1",
labels: %{"labels" => ["ubuntu-latest"]}
})
{:ok, token: token}
end
test "a runner polling too fast is told there's no work", %{conn: conn, token: token} do
poll = fn ->
body =
FetchTaskRequest.encode(%FetchTaskRequest{tasks_version: 4}) |> IO.iodata_to_binary()
resp =
conn
|> put_req_header("content-type", "application/protobuf")
|> put_req_header("authorization", "Bearer " <> token)
|> post("/api/actions/_apis/runner.v1.RunnerService/FetchTask", body)
assert resp.status == 200
FetchTaskResponse.decode(resp.resp_body)
end
poll.()
poll.()
queue_job()
# Over the limit: not an error, and the queued job stays queued.
assert %FetchTaskResponse{task: nil, tasks_version: 5} = poll.()
assert GitGud.Repo.one!(from j in GitGud.Workflows.WorkflowJob, select: j.status) ==
"queued"
end
end
defp queue_job do
{_user, repo} = GitGud.ForgeFixtures.repository_fixture()
run =
%GitGud.Workflows.WorkflowRun{repository_id: repo.id}
|> GitGud.Workflows.WorkflowRun.create_changeset(%{
number: 1,
workflow_path: ".forgejo/workflows/ci.yml",
trigger: "push",
head_sha: :crypto.strong_rand_bytes(20)
})
|> GitGud.Repo.insert!()
%GitGud.Workflows.WorkflowJob{workflow_run_id: run.id}
|> GitGud.Workflows.WorkflowJob.create_changeset(%{
job_id: "build",
runs_on: %{"labels" => ["ubuntu-latest"]},
definition: %{"runs-on" => "ubuntu-latest", "steps" => [%{"run" => "true"}]}
})
|> GitGud.Repo.insert!()
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
6.8 KiB · text
History
b868907