@@ -5,15 +5,21 @@ defmodule GitGud.Workflows.Runners do
|
5
|
5
|
Two registration paths: |
|
6
|
6
|
|
|
7
|
7
|
1. **Per-scope token** — repo or org admins mint a token via the |
|
8
|
|
− settings UI; the resulting runner is scoped to that repo or org. |
|
9
|
|
− Stored as a SHA-256 hash on `repositories.runner_reg_token_hash` |
|
10
|
|
− or `organizations.runner_reg_token_hash`. Regenerating shifts the |
|
11
|
|
− hash, invalidating the old token in one step. A token registers |
|
12
|
|
− one runner and expires a day after it's minted. |
|
|
8
|
+ settings UI, and a user for their own repositories on |
|
|
9
|
+ /users/settings/runners; the resulting runner is scoped to that |
|
|
10
|
+ repo, org or user. Stored as a SHA-256 hash on the |
|
|
11
|
+ `runner_reg_token_hash` of `repositories`, `organizations` or |
|
|
12
|
+ `users`. Regenerating shifts the hash, invalidating the old token |
|
|
13
|
+ in one step. A token registers one runner and expires a day after |
|
|
14
|
+ it's minted. |
|
13
|
15
|
|
|
14
|
16
|
2. **Global env token** (`GITGUD_RUNNER_REG_TOKEN`) — legacy path |
|
15
|
|
− for operator-managed pool runners. Resulting runner has neither |
|
16
|
|
− repository_id nor organization_id set and serves any repo's jobs. |
|
|
17
|
+ for operator-managed pool runners. Resulting runner has no |
|
|
18
|
+ repository, organization or owner and serves any repo's jobs. |
|
|
19
|
+ |
|
|
20
|
+ A user's runner (`owner_id`) serves the repositories that user owns |
|
|
21
|
+ personally — not those of an organization they belong to, though an |
|
|
22
|
+ org repository's `owner_id` is its creator's. |
|
17
|
23
|
|
|
18
|
24
|
Runner-side tokens (issued by `register/2`) are random 32 bytes; we |
|
19
|
25
|
store only the SHA-256 hash. The bare token is shown once and never |
@@ -22,6 +28,7 @@ defmodule GitGud.Workflows.Runners do
|
22
|
28
|
|
|
23
|
29
|
import Ecto.Query, warn: false |
|
24
|
30
|
|
|
|
31
|
+ alias GitGud.Accounts.User |
|
25
|
32
|
alias GitGud.Organizations.Organization |
|
26
|
33
|
alias GitGud.Repo |
|
27
|
34
|
alias GitGud.Repositories.Repository |
@@ -52,6 +59,7 @@ defmodule GitGud.Workflows.Runners do
|
52
|
59
|
* `{:ok, :global}` — legacy global token from env / config |
|
53
|
60
|
* `{:ok, %Repository{}}` — repo-scoped token |
|
54
|
61
|
* `{:ok, %Organization{}}` — org-scoped token |
|
|
62
|
+ * `{:ok, %User{}}` — a user's own token |
|
55
|
63
|
* `:error` — unknown, expired or already used |
|
56
|
64
|
|
|
57
|
65
|
A scoped token is good for one runner, within |
@@ -76,6 +84,9 @@ defmodule GitGud.Workflows.Runners do
|
76
|
84
|
org = redeem(Organization, token) -> |
|
77
|
85
|
{:ok, org} |
|
78
|
86
|
|
|
|
87
|
+ user = redeem(User, token) -> |
|
|
88
|
+ {:ok, user} |
|
|
89
|
+ |
|
79
|
90
|
true -> |
|
80
|
91
|
:error |
|
81
|
92
|
end |
@@ -164,7 +175,8 @@ defmodule GitGud.Workflows.Runners do
|
164
|
175
|
Regenerating is destructive — any previously-distributed token for |
|
165
|
176
|
that scope stops working immediately. |
|
166
|
177
|
""" |
|
167
|
|
− def mint_registration_token(%scope{} = record) when scope in [Repository, Organization] do |
|
|
178
|
+ def mint_registration_token(%scope{} = record) |
|
|
179
|
+ when scope in [Repository, Organization, User] do |
|
168
|
180
|
token = mint_token() |
|
169
|
181
|
expires_at = DateTime.add(DateTime.utc_now(:second), registration_token_ttl_seconds()) |
|
170
|
182
|
|
@@ -194,7 +206,7 @@ defmodule GitGud.Workflows.Runners do
|
194
|
206
|
@doc """ |
|
195
|
207
|
Register a new runner. `attrs` must include `:name`. `scope` is the |
|
196
|
208
|
value returned by `consume_registration_token/1` and decides whether |
|
197
|
|
− the runner ends up repo-scoped, org-scoped, or global. |
|
|
209
|
+ the runner ends up repo-scoped, org-scoped, a user's, or global. |
|
198
|
210
|
|
|
199
|
211
|
Returns `{:ok, runner, bare_token}` on success; bare token is the |
|
200
|
212
|
runner's bearer credential, shown once. |
@@ -229,6 +241,8 @@ defmodule GitGud.Workflows.Runners do
|
229
|
241
|
@doc "Where a runner of `scope` belongs in the audit log." |
|
230
|
242
|
def audit_scope(%Repository{} = repo), do: %{repository: repo} |
|
231
|
243
|
def audit_scope(%Organization{} = org), do: %{organization: org} |
|
|
244
|
+ # A user's runner is about them: their security log. |
|
|
245
|
+ def audit_scope(%User{} = user), do: %{subject: user} |
|
232
|
246
|
def audit_scope(_global), do: %{} |
|
233
|
247
|
|
|
234
|
248
|
@doc "A runner's advertised labels, as a list." |
@@ -238,6 +252,7 @@ defmodule GitGud.Workflows.Runners do
|
238
|
252
|
defp scope_attrs(:global), do: %{} |
|
239
|
253
|
defp scope_attrs(%Repository{id: id}), do: %{repository_id: id} |
|
240
|
254
|
defp scope_attrs(%Organization{id: id}), do: %{organization_id: id} |
|
|
255
|
+ defp scope_attrs(%User{id: id}), do: %{owner_id: id} |
|
241
|
256
|
defp scope_attrs(_), do: %{} |
|
242
|
257
|
|
|
243
|
258
|
@doc """ |
@@ -424,21 +439,24 @@ defmodule GitGud.Workflows.Runners do
|
424
|
439
|
|
|
425
|
440
|
@doc """ |
|
426
|
441
|
The PubSub topic a runners page follows for the runners of `scope`: |
|
427
|
|
− a repository's, an organization's, the global ones (`:global`), or |
|
428
|
|
− every runner on the instance (`:all`). |
|
|
442
|
+ a repository's, an organization's, a user's own, the global ones |
|
|
443
|
+ (`:global`), or every runner on the instance (`:all`). |
|
429
|
444
|
""" |
|
430
|
445
|
def topic(%Repository{id: id}), do: "runners:repo:#{id}" |
|
431
|
446
|
def topic(%Organization{id: id}), do: "runners:org:#{id}" |
|
|
447
|
+ def topic(%User{id: id}), do: "runners:user:#{id}" |
|
432
|
448
|
def topic(:global), do: "runners:global" |
|
433
|
449
|
def topic(:all), do: "runners:all" |
|
434
|
450
|
|
|
435
|
451
|
defp topic_for(%Runner{repository_id: rid}) when is_integer(rid), do: "runners:repo:#{rid}" |
|
436
|
452
|
defp topic_for(%Runner{organization_id: oid}) when is_integer(oid), do: "runners:org:#{oid}" |
|
|
453
|
+ defp topic_for(%Runner{owner_id: uid}) when is_integer(uid), do: "runners:user:#{uid}" |
|
437
|
454
|
defp topic_for(%Runner{}), do: topic(:global) |
|
438
|
455
|
|
|
439
|
456
|
@doc """ |
|
440
|
457
|
Delete a runner. Caller is responsible for authorization (repo admin |
|
441
|
|
− for repo-scoped, org admin for org-scoped, app admin for global). |
|
|
458
|
+ for repo-scoped, org admin for org-scoped, the user for their own, |
|
|
459
|
+ app admin for global). |
|
442
|
460
|
""" |
|
443
|
461
|
def delete_runner(%Runner{} = runner), do: Repo.delete(runner) |
|
444
|
462
|
|
@@ -455,62 +473,50 @@ defmodule GitGud.Workflows.Runners do
|
455
|
473
|
List runners visible at `scope`. |
|
456
|
474
|
|
|
457
|
475
|
* `%Repository{}` — runners scoped to that repo + that repo's org |
|
458
|
|
− (if any) + global runners |
|
|
476
|
+ (if any), or its owner's own runners for a personal repo + global |
|
|
477
|
+ runners |
|
459
|
478
|
* `%Organization{}` — runners scoped to that org + global |
|
|
479
|
+ * `%User{}` — that user's own runners + global |
|
460
|
480
|
* `:global` — only global runners |
|
461
|
481
|
""" |
|
462
|
|
− def list_for_scope(%Repository{id: rid, organization_id: oid}) do |
|
463
|
|
− base = |
|
464
|
|
− from r in Runner, |
|
465
|
|
− order_by: [desc: r.last_seen_at] |
|
466
|
|
− |
|
467
|
|
− cond do |
|
468
|
|
− is_integer(oid) -> |
|
469
|
|
− Repo.all( |
|
470
|
|
− from r in base, |
|
471
|
|
− where: |
|
472
|
|
− r.repository_id == ^rid or |
|
473
|
|
− r.organization_id == ^oid or |
|
474
|
|
− (is_nil(r.repository_id) and is_nil(r.organization_id)) |
|
475
|
|
− ) |
|
476
|
|
− |
|
477
|
|
− true -> |
|
478
|
|
− Repo.all( |
|
479
|
|
− from r in base, |
|
480
|
|
− where: |
|
481
|
|
− r.repository_id == ^rid or |
|
482
|
|
− (is_nil(r.repository_id) and is_nil(r.organization_id)) |
|
483
|
|
− ) |
|
484
|
|
− end |
|
|
482
|
+ def list_for_scope(%Repository{id: rid, organization_id: oid}) when is_integer(oid) do |
|
|
483
|
+ from(r in Runner, where: r.repository_id == ^rid or r.organization_id == ^oid) |
|
|
484
|
+ |> or_global() |
|
485
|
485
|
end |
|
486
|
486
|
|
|
487
|
|
− def list_for_scope(%Organization{id: oid}) do |
|
488
|
|
− Repo.all( |
|
489
|
|
− from r in Runner, |
|
490
|
|
− where: |
|
491
|
|
− r.organization_id == ^oid or |
|
492
|
|
− (is_nil(r.repository_id) and is_nil(r.organization_id)), |
|
493
|
|
− order_by: [desc: r.last_seen_at] |
|
494
|
|
− ) |
|
|
487
|
+ def list_for_scope(%Repository{id: rid, owner_id: uid}) when is_integer(uid) do |
|
|
488
|
+ from(r in Runner, where: r.repository_id == ^rid or r.owner_id == ^uid) |
|
|
489
|
+ |> or_global() |
|
495
|
490
|
end |
|
496
|
491
|
|
|
497
|
|
− def list_for_scope(:global) do |
|
498
|
|
− Repo.all( |
|
499
|
|
− from r in Runner, |
|
500
|
|
− where: is_nil(r.repository_id) and is_nil(r.organization_id), |
|
501
|
|
− order_by: [desc: r.last_seen_at] |
|
|
492
|
+ def list_for_scope(%Repository{id: rid}), |
|
|
493
|
+ do: from(r in Runner, where: r.repository_id == ^rid) |> or_global() |
|
|
494
|
+ |
|
|
495
|
+ def list_for_scope(%Organization{id: oid}), |
|
|
496
|
+ do: from(r in Runner, where: r.organization_id == ^oid) |> or_global() |
|
|
497
|
+ |
|
|
498
|
+ def list_for_scope(%User{id: uid}), |
|
|
499
|
+ do: from(r in Runner, where: r.owner_id == ^uid) |> or_global() |
|
|
500
|
+ |
|
|
501
|
+ def list_for_scope(:global), do: from(r in Runner, where: false) |> or_global() |
|
|
502
|
+ |
|
|
503
|
+ defp or_global(query) do |
|
|
504
|
+ from(r in query, |
|
|
505
|
+ or_where: is_nil(r.repository_id) and is_nil(r.organization_id) and is_nil(r.owner_id), |
|
|
506
|
+ order_by: [desc: r.last_seen_at] |
|
502
|
507
|
) |
|
|
508
|
+ |> Repo.all() |
|
503
|
509
|
end |
|
504
|
510
|
|
|
505
|
511
|
@doc """ |
|
506
|
|
− Every runner on the instance, whatever its scope, with the repository |
|
507
|
|
− or organization it belongs to — for the instance admins' page. |
|
|
512
|
+ Every runner on the instance, whatever its scope, with the repository, |
|
|
513
|
+ organization or user it belongs to — for the instance admins' page. |
|
508
|
514
|
""" |
|
509
|
515
|
def list_all do |
|
510
|
516
|
Repo.all( |
|
511
|
517
|
from r in Runner, |
|
512
|
518
|
order_by: [desc_nulls_last: r.last_seen_at, asc: r.name], |
|
513
|
|
− preload: [repository: [:owner, :organization], organization: []] |
|
|
519
|
+ preload: [repository: [:owner, :organization], organization: [], owner: []] |
|
514
|
520
|
) |
|
515
|
521
|
end |
|
516
|
522
|
end |