neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
Log in to mark this repository.
Federation: only public repositories federate; remotes act only on what's theirs
4d7e44f · Gabriel Morell · 2026-10-18 19:44
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud/federation.ex
+23
−3
| 245 | 245 | so audit and replay remain possible. |
| 246 | 246 | """ |
| 247 | 247 | def screen_inbound(actor_url) when is_binary(actor_url) do |
| 248 | − host = URI.parse(actor_url).host || "" | |
| 248 | + host = normalize_host(URI.parse(actor_url).host) | |
| 249 | 249 | |
| 250 | 250 | cond do |
| 251 | 251 | Repo.exists?(from b in ActorBlock, where: b.actor_url == ^actor_url) -> |
| 252 | 252 | {:drop, "actor_blocked"} |
| 253 | 253 | |
| 254 | − Repo.exists?(from b in InstanceBlock, where: b.host == ^host) -> | |
| 254 | + # Hosts compare case-insensitively (DNS does), and a block covers | |
| 255 | + # the host's subdomains too: blocking evil.example must not be | |
| 256 | + # undone by EVIL.example or a.evil.example. | |
| 257 | + Repo.exists?( | |
| 258 | + from b in InstanceBlock, where: fragment("lower(?)", b.host) in ^host_and_parents(host) | |
| 259 | + ) -> | |
| 255 | 260 | {:drop, "instance_blocked"} |
| 256 | 261 | |
| 257 | 262 | federation_mode() == :allowlist and |
| 258 | − not Repo.exists?(from a in InstanceAllowlist, where: a.host == ^host) -> | |
| 263 | + not Repo.exists?( | |
| 264 | + from a in InstanceAllowlist, where: fragment("lower(?)", a.host) == ^host | |
| 265 | + ) -> | |
| 259 | 266 | {:drop, "not_on_allowlist"} |
| 260 | 267 | |
| 261 | 268 | true -> |
| 263 | 270 | end |
| 264 | 271 | end |
| 265 | 272 | |
| 273 | + @doc "A host as federation compares it: lowercased, without a trailing dot." | |
| 274 | + def normalize_host(nil), do: "" | |
| 275 | + | |
| 276 | + def normalize_host(host) when is_binary(host), | |
| 277 | + do: host |> String.trim() |> String.trim_trailing(".") |> String.downcase() | |
| 278 | + | |
| 279 | + defp host_and_parents(""), do: [""] | |
| 280 | + | |
| 281 | + defp host_and_parents(host) do | |
| 282 | + labels = String.split(host, ".") | |
| 283 | + for i <- 0..(length(labels) - 1), do: labels |> Enum.drop(i) |> Enum.join(".") | |
| 284 | + end | |
| 285 | + | |
| 266 | 286 | # ── moderation CRUD ────────────────────────────────────────────────── |
| 267 | 287 | |
| 268 | 288 | def block_instance(host, opts \\ []) do |
modified
lib/git_gud/federation/instance_allowlist.ex
+1
−0
| 12 | 12 | def changeset(a, attrs) do |
| 13 | 13 | a |
| 14 | 14 | |> cast(attrs, [:host, :note]) |
| 15 | + |> update_change(:host, &GitGud.Federation.normalize_host/1) | |
| 15 | 16 | |> validate_required([:host]) |
| 16 | 17 | |> validate_length(:host, max: 250) |
| 17 | 18 | |> unique_constraint(:host) |
modified
lib/git_gud/federation/instance_block.ex
+1
−0
| 15 | 15 | def changeset(b, attrs) do |
| 16 | 16 | b |
| 17 | 17 | |> cast(attrs, [:host, :severity, :reason]) |
| 18 | + |> update_change(:host, &GitGud.Federation.normalize_host/1) | |
| 18 | 19 | |> validate_required([:host]) |
| 19 | 20 | |> validate_length(:host, max: 250) |
| 20 | 21 | |> validate_inclusion(:severity, @severities) |
modified
lib/git_gud/federation/publishing.ex
+26
−0
| 95 | 95 | alias GitGud.PullRequests.PullRequest |
| 96 | 96 | alias GitGud.Repositories.Repository |
| 97 | 97 | |
| 98 | + # Only public repositories and organizations federate. A follower of | |
| 99 | + # a repository that is (or has since become) internal or private gets | |
| 100 | + # nothing: these activities carry commit messages and author emails, | |
| 101 | + # issue and PR titles and bodies, and comments. | |
| 102 | + def publish_org_repo_created(%Organization{visibility: ov}, %Repository{visibility: rv}) | |
| 103 | + when ov != "public" or rv != "public", | |
| 104 | + do: :ok | |
| 105 | + | |
| 98 | 106 | def publish_org_repo_created(%Organization{} = org, %Repository{} = repo) do |
| 99 | 107 | with {:ok, org_actor} <- Federation.ensure_local_actor(org), |
| 100 | 108 | {:ok, repo_actor} <- Federation.ensure_local_actor(repo) do |
| 103 | 111 | end |
| 104 | 112 | end |
| 105 | 113 | |
| 114 | + def publish_org_member_added(%Organization{visibility: v}, _user, _role) when v != "public", | |
| 115 | + do: :ok | |
| 116 | + | |
| 106 | 117 | def publish_org_member_added(%Organization{} = org, %GitGud.Accounts.User{} = user, role) do |
| 107 | 118 | with {:ok, org_actor} <- Federation.ensure_local_actor(org), |
| 108 | 119 | {:ok, member_actor} <- Federation.ensure_local_actor(user) do |
| 111 | 122 | end |
| 112 | 123 | end |
| 113 | 124 | |
| 125 | + def publish_push(%Repository{visibility: v}, _params) when v != "public", do: :ok | |
| 126 | + | |
| 114 | 127 | def publish_push(%Repository{} = repo, params) do |
| 115 | 128 | {:ok, repo_actor} = Federation.ensure_local_actor(repo) |
| 116 | 129 | doc = ActivityBuilders.push(repo_actor, params) |
| 117 | 130 | publish(repo_actor, doc) |
| 118 | 131 | end |
| 119 | 132 | |
| 133 | + def publish_issue_opened(%Repository{visibility: v}, _issue, _author) when v != "public", | |
| 134 | + do: :ok | |
| 135 | + | |
| 120 | 136 | def publish_issue_opened(%Repository{} = repo, %Issue{} = issue, author) do |
| 121 | 137 | with {:ok, repo_actor} <- Federation.ensure_local_actor(repo), |
| 122 | 138 | {:ok, author_actor} <- maybe_actor_for(author) do |
| 126 | 142 | end |
| 127 | 143 | end |
| 128 | 144 | |
| 145 | + def publish_issue_comment(%Repository{visibility: v}, _issue, _comment, _author) | |
| 146 | + when v != "public", do: :ok | |
| 147 | + | |
| 129 | 148 | def publish_issue_comment(%Repository{} = repo, %Issue{} = issue, comment, author) do |
| 130 | 149 | with {:ok, repo_actor} <- Federation.ensure_local_actor(repo), |
| 131 | 150 | {:ok, author_actor} <- maybe_actor_for(author) do |
| 139 | 158 | end |
| 140 | 159 | end |
| 141 | 160 | |
| 161 | + def publish_pr_opened(%Repository{visibility: v}, _pr, _author) when v != "public", do: :ok | |
| 162 | + | |
| 142 | 163 | def publish_pr_opened(%Repository{} = repo, %PullRequest{} = pr, author) do |
| 143 | 164 | with {:ok, repo_actor} <- Federation.ensure_local_actor(repo), |
| 144 | 165 | {:ok, author_actor} <- maybe_actor_for(author) do |
| 148 | 169 | end |
| 149 | 170 | end |
| 150 | 171 | |
| 172 | + def publish_pr_comment(%Repository{visibility: v}, _pr, _comment, _author) when v != "public", | |
| 173 | + do: :ok | |
| 174 | + | |
| 151 | 175 | def publish_pr_comment(%Repository{} = repo, %PullRequest{} = pr, comment, author) do |
| 152 | 176 | with {:ok, repo_actor} <- Federation.ensure_local_actor(repo), |
| 153 | 177 | {:ok, author_actor} <- maybe_actor_for(author) do |
| 167 | 191 | end |
| 168 | 192 | end |
| 169 | 193 | |
| 194 | + def publish_pr_merged(%Repository{visibility: v}, _pr) when v != "public", do: :ok | |
| 195 | + | |
| 170 | 196 | def publish_pr_merged(%Repository{} = repo, %PullRequest{} = pr) do |
| 171 | 197 | {:ok, repo_actor} = Federation.ensure_local_actor(repo) |
| 172 | 198 | doc = ActivityBuilders.pr_merged(repo_actor, pr) |
modified
lib/git_gud/federation/rate_limiter.ex
+2
−0
| 73 | 73 | def check(nil), do: :ok |
| 74 | 74 | |
| 75 | 75 | def check(host) when is_binary(host) do |
| 76 | + # One bucket per host however it's spelled (EVIL.example, evil.example.). | |
| 77 | + host = GitGud.Federation.normalize_host(host) | |
| 76 | 78 | threshold = effective_threshold(host) |
| 77 | 79 | count = :ets.update_counter(@table, host, {2, 1}, {host, 0}) |
| 78 | 80 |
modified
lib/git_gud/federation/remote_actors.ex
+57
−8
Click to load diff…
modified
lib/git_gud/federation/workers/process_inbox.ex
+42
−8
Click to load diff…
modified
lib/git_gud/pull_requests.ex
+16
−2
Click to load diff…
modified
lib/git_gud_web/controllers/ap_actor_controller.ex
+79
−38
Click to load diff…
modified
test/git_gud/federation/interaction_policy_test.exs
+1
−1
Click to load diff…
modified
test/git_gud/federation/publishing_test.exs
+3
−3
Click to load diff…
modified
test/git_gud/pull_requests_federated_review_test.exs
+15
−9
Click to load diff…
modified
test/git_gud/pull_requests_federated_test.exs
+159
−2
Click to load diff…
added
test/git_gud_web/controllers/ap_federation_hardening_test.exs
+236
−0
Click to load diff…
Parents: fea96e8