lib/openagents/reputation/subject_claim.ex

main at 58e6347eeb72 · 2 KB

defmodule OpenAgents.Reputation.SubjectClaim do
  @moduledoc """
  One account's claim on one attestation subject.

  `OpenAgents.Reputation.Attestation` names its subject with a bare string the
  issuer supplies. This row says what that string names and which account holds
  it, and only a `linked` status resolves it — the rule
  `OpenAgents.Forum.ActorLink` already applies to a legacy forum identity.

  A subject is one of three things, and collapsing them is what produced a bare
  string in the first place:

    * `account` — this forge's own actor reference, `user:<account-id>`. The
      string names the account, so the database checks it rather than an
      operator.
    * `forum_actor` — a legacy forum identity that may have no account of its
      own. The claim carries the `forum_actor_links` row that established it.
    * `agent` — a registered agent. The claim carries the `agents` row.

  The kind is not on the attestation. An attestation's `subject_id` is inside
  the signed claim, so a kind column there would either go unsigned or
  invalidate every published signature.
  """

  use Ecto.Schema
  import Ecto.Changeset

  alias OpenAgents.Accounts.User
  alias OpenAgents.Agents.Agent
  alias OpenAgents.Forum.ActorLink

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  @subject_kinds ~w(account forum_actor agent)
  @statuses ~w(pending linked rejected)

  schema "reputation_subject_claims" do
    field :subject_kind, :string
    field :subject_id, :string
    field :status, :string, default: "pending"
    field :proof_method, :string
    field :proof_evidence, :map
    field :linked_at, :utc_datetime_usec
    field :rejected_at, :utc_datetime_usec

    belongs_to :user, User
    belongs_to :forum_actor_link, ActorLink
    belongs_to :agent, Agent

    timestamps(type: :utc_datetime_usec)
  end

  @type t :: %__MODULE__{}

  def subject_kinds, do: @subject_kinds
  def statuses, do: @statuses

  def changeset(claim, attributes) do
    claim
    |> cast(
      attributes,
      ~w(subject_kind subject_id user_id forum_actor_link_id agent_id status proof_method
         proof_evidence linked_at rejected_at)a
    )
    |> validate_required(~w(subject_kind subject_id user_id status)a)
    |> validate_inclusion(:subject_kind, @subject_kinds)
    |> validate_inclusion(:status, @statuses)
    |> validate_length(:subject_id, min: 1, max: 256)
    |> unique_constraint(:subject_id)
    |> foreign_key_constraint(:user_id)
    |> foreign_key_constraint(:forum_actor_link_id)
    |> foreign_key_constraint(:agent_id)
    |> check_constraint(:subject_kind, name: :reputation_subject_claims_kind_check)
    |> check_constraint(:status, name: :reputation_subject_claims_status_check)
    |> check_constraint(:subject_id, name: :reputation_subject_claims_reference_check)
  end
end