2.7 KiB · text History b868907
defmodule GitGud.Workflows.JobTokenGrant do
  @moduledoc """
  One repository letting another's CI jobs in.

  A job's `${{ github.token }}` is limited to its own repository. A grant
  on repository B names a source — one repository, or every repository of
  an organization — whose job tokens may additionally:

    * `read` B's container images and packages, and
    * `dispatch` B's workflows (`workflow_dispatch`).

  It is GitLab's job-token allowlist: the repository being reached into
  decides, not the one reaching.
  """
  use Ecto.Schema

  import Ecto.Changeset

  alias GitGud.Organizations.Organization
  alias GitGud.Repositories.Repository

  schema "job_token_grants" do
    field :read, :boolean, default: true
    field :dispatch, :boolean, default: false

    belongs_to :repository, Repository
    belongs_to :source_repository, Repository
    belongs_to :source_organization, Organization

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(grant, attrs) do
    grant
    |> cast(attrs, [
      :repository_id,
      :source_repository_id,
      :source_organization_id,
      :read,
      :dispatch
    ])
    |> validate_required([:repository_id])
    |> validate_one_source()
    |> validate_grants_something()
    |> check_constraint(:source_repository_id, name: :exactly_one_source)
    |> unique_constraint(:source_repository_id,
      name: :job_token_grants_repo_source_repo_index,
      message: "already has a grant"
    )
    |> unique_constraint(:source_organization_id,
      name: :job_token_grants_repo_source_org_index,
      message: "already has a grant"
    )
  end

  defp validate_one_source(changeset) do
    case {get_field(changeset, :source_repository_id),
          get_field(changeset, :source_organization_id)} do
      {nil, nil} ->
        add_error(changeset, :source_repository_id, "name a repository or an organization")

      {_, nil} ->
        validate_not_self(changeset)

      {nil, _} ->
        changeset

      _ ->
        add_error(
          changeset,
          :source_repository_id,
          "name a repository or an organization, not both"
        )
    end
  end

  # A repository's own jobs already have full access to it.
  defp validate_not_self(changeset) do
    if get_field(changeset, :source_repository_id) == get_field(changeset, :repository_id) do
      add_error(
        changeset,
        :source_repository_id,
        "is this repository; its jobs already have access"
      )
    else
      changeset
    end
  end

  defp validate_grants_something(changeset) do
    if get_field(changeset, :read) or get_field(changeset, :dispatch) do
      changeset
    else
      add_error(changeset, :read, "grant at least one of read or dispatch")
    end
  end
end