diff --git a/apps/guildhall_ops_db/lib/guildhall/ops_db/guild.ex b/apps/guildhall_ops_db/lib/guildhall/ops_db/guild.ex new file mode 100644 index 0000000..509c6ea --- /dev/null +++ b/apps/guildhall_ops_db/lib/guildhall/ops_db/guild.ex @@ -0,0 +1,57 @@ +defmodule Guildhall.OpsDb.Guild do + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{} + + @min_guild_id 0x010 + @max_guild_id 0x3FF + + schema "guilds" do + field :guild_id, :integer + field :name, :string + field :slug, :string + field :guild_type, :string + field :description, :string + field :trust_domain, :string + field :contact_did, :string + field :registrant_did, :string + field :registration_ceremony_id, :string + field :status, :string, default: "pending_approval" + field :enrollment_accord_ref, :string + field :metadata, :map, default: %{} + + has_many :guild_schematics, Guildhall.OpsDb.GuildSchematic + has_many :guild_memberships, Guildhall.OpsDb.GuildMembership + + timestamps(type: :utc_datetime_usec) + end + + def changeset(guild, attrs) do + guild + |> cast(attrs, [ + :guild_id, + :name, + :slug, + :guild_type, + :description, + :trust_domain, + :contact_did, + :registrant_did, + :registration_ceremony_id, + :status, + :enrollment_accord_ref, + :metadata + ]) + |> validate_required([:guild_id, :name, :slug, :guild_type, :contact_did, :registrant_did]) + |> validate_inclusion(:guild_type, ~w(msp isv nsp)) + |> validate_inclusion(:status, ~w(pending_approval approved denied active suspended)) + |> validate_number(:guild_id, + greater_than_or_equal_to: @min_guild_id, + less_than_or_equal_to: @max_guild_id + ) + |> unique_constraint(:guild_id) + |> unique_constraint(:slug) + |> unique_constraint(:name) + end +end diff --git a/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_membership.ex b/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_membership.ex new file mode 100644 index 0000000..e1c8365 --- /dev/null +++ b/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_membership.ex @@ -0,0 +1,45 @@ +defmodule Guildhall.OpsDb.GuildMembership do + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{} + + schema "guild_memberships" do + belongs_to :guild, Guildhall.OpsDb.Guild + + field :user_did, :string + field :user_email, :string + field :display_name, :string + field :keycloak_sub, :string + field :role, :string, default: "apprentice" + field :status, :string, default: "pending" + field :membership_ceremony_id, :string + field :approved_by_did, :string + field :approved_at, :utc_datetime_usec + field :metadata, :map, default: %{} + + timestamps(type: :utc_datetime_usec) + end + + def changeset(membership, attrs) do + membership + |> cast(attrs, [ + :guild_id, + :user_did, + :user_email, + :display_name, + :keycloak_sub, + :role, + :status, + :membership_ceremony_id, + :approved_by_did, + :approved_at, + :metadata + ]) + |> validate_required([:guild_id, :user_did, :user_email, :keycloak_sub]) + |> validate_inclusion(:role, ~w(apprentice journeyman master)) + |> validate_inclusion(:status, ~w(pending approved denied active suspended removed)) + |> foreign_key_constraint(:guild_id) + |> unique_constraint([:guild_id, :user_did]) + end +end diff --git a/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_memberships.ex b/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_memberships.ex new file mode 100644 index 0000000..c7bd4c8 --- /dev/null +++ b/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_memberships.ex @@ -0,0 +1,58 @@ +defmodule Guildhall.OpsDb.GuildMemberships do + import Ecto.Query, only: [from: 2] + alias Guildhall.OpsDb.{Repo, GuildMembership} + + def list_memberships(guild_id) do + Repo.all(from(m in GuildMembership, where: m.guild_id == ^guild_id, order_by: [asc: m.inserted_at])) + end + + def get_membership!(id), do: Repo.get!(GuildMembership, id) + + def active_members(guild_id) do + Repo.all(from(m in GuildMembership, where: m.guild_id == ^guild_id and m.status == "active")) + end + + def masters_for_guild(guild_id) do + Repo.all( + from(m in GuildMembership, + where: m.guild_id == ^guild_id and m.role == "master" and m.status == "active" + ) + ) + end + + def find_membership(guild_id, user_did) do + Repo.one(from(m in GuildMembership, where: m.guild_id == ^guild_id and m.user_did == ^user_did)) + end + + def request_membership(attrs) do + %GuildMembership{} + |> GuildMembership.changeset(attrs) + |> Repo.insert() + end + + def approve_membership(%GuildMembership{} = membership, approver_did) do + membership + |> GuildMembership.changeset(%{ + status: "active", + approved_by_did: approver_did, + approved_at: DateTime.utc_now() + }) + |> Repo.update() + end + + def deny_membership(%GuildMembership{} = membership, approver_did) do + membership + |> GuildMembership.changeset(%{ + status: "denied", + approved_by_did: approver_did, + approved_at: DateTime.utc_now() + }) + |> Repo.update() + end + + def update_role(%GuildMembership{} = membership, new_role) do + membership + |> GuildMembership.changeset(%{role: new_role}) + |> Repo.update() + end +end diff --git a/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_schematic.ex b/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_schematic.ex new file mode 100644 index 0000000..3063412 --- /dev/null +++ b/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_schematic.ex @@ -0,0 +1,42 @@ +defmodule Guildhall.OpsDb.GuildSchematic do + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{} + + schema "guild_schematics" do + belongs_to :guild, Guildhall.OpsDb.Guild + + field :template_name, :string + field :schematic_name, :string + field :schematic_version, :string + field :tree_hash, :string + field :binding_id, :string + field :realization_id, :string + field :status, :string, default: "pending" + field :customization_params, :map, default: %{} + field :realization_snapshot, :map, default: %{} + + timestamps(type: :utc_datetime_usec) + end + + def changeset(schematic, attrs) do + schematic + |> cast(attrs, [ + :guild_id, + :template_name, + :schematic_name, + :schematic_version, + :tree_hash, + :binding_id, + :realization_id, + :status, + :customization_params, + :realization_snapshot + ]) + |> validate_required([:guild_id, :template_name, :schematic_name, :schematic_version]) + |> validate_inclusion(:status, ~w(pending forked binding_created realizing realized partially_realized failed)) + |> foreign_key_constraint(:guild_id) + |> unique_constraint([:schematic_name, :schematic_version]) + end +end diff --git a/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_schematics.ex b/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_schematics.ex new file mode 100644 index 0000000..cb69147 --- /dev/null +++ b/apps/guildhall_ops_db/lib/guildhall/ops_db/guild_schematics.ex @@ -0,0 +1,30 @@ +defmodule Guildhall.OpsDb.GuildSchematics do + import Ecto.Query, only: [from: 2] + alias Guildhall.OpsDb.{Repo, GuildSchematic} + + def get_for_guild(guild_id) do + Repo.one( + from(gs in GuildSchematic, + where: gs.guild_id == ^guild_id, + order_by: [desc: gs.inserted_at], + limit: 1 + ) + ) + end + + def create(attrs) do + %GuildSchematic{} + |> GuildSchematic.changeset(attrs) + |> Repo.insert() + end + + def update_schematic(%GuildSchematic{} = gs, attrs) do + gs + |> GuildSchematic.changeset(attrs) + |> Repo.update() + end + + def update_realization_snapshot(%GuildSchematic{} = gs, snapshot) do + update_schematic(gs, %{realization_snapshot: snapshot}) + end +end diff --git a/apps/guildhall_ops_db/lib/guildhall/ops_db/guilds.ex b/apps/guildhall_ops_db/lib/guildhall/ops_db/guilds.ex new file mode 100644 index 0000000..9211c3d --- /dev/null +++ b/apps/guildhall_ops_db/lib/guildhall/ops_db/guilds.ex @@ -0,0 +1,63 @@ +defmodule Guildhall.OpsDb.Guilds do + import Ecto.Query + alias Guildhall.OpsDb.{Repo, Guild, GuildMembership} + alias Ecto.Multi + + def list_guilds(filters \\ []) do + Guild + |> maybe_filter_status(filters[:status]) + |> order_by([g], desc: g.inserted_at) + |> Repo.all() + end + + def get_guild!(id), do: Repo.get!(Guild, id) + + def get_guild_by_slug(slug), do: Repo.get_by(Guild, slug: slug) + + def create_guild(attrs) do + %Guild{} + |> Guild.changeset(attrs) + |> Repo.insert() + end + + def update_guild(%Guild{} = guild, attrs) do + guild + |> Guild.changeset(attrs) + |> Repo.update() + end + + def next_guild_id do + case Repo.one(from(g in Guild, select: max(g.guild_id))) do + nil -> 0x010 + max_id -> max_id + 1 + end + end + + def approve_guild(%Guild{} = guild) do + now = DateTime.utc_now() + + Multi.new() + |> Multi.update(:guild, Guild.changeset(guild, %{status: "approved"})) + |> Multi.insert(:master_membership, GuildMembership.changeset(%GuildMembership{}, %{ + guild_id: guild.id, + user_did: guild.registrant_did, + user_email: guild.contact_did, + display_name: "Guild Registrant", + keycloak_sub: "", + role: "master", + status: "active", + approved_by_did: "system", + approved_at: now + }), on_conflict: :nothing, conflict_target: [:guild_id, :user_did]) + |> Repo.transaction() + end + + def guild_count(status \\ nil) do + Guild + |> maybe_filter_status(status) + |> Repo.aggregate(:count) + end + + defp maybe_filter_status(query, nil), do: query + defp maybe_filter_status(query, status), do: where(query, [g], g.status == ^status) +end diff --git a/apps/guildhall_ops_db/priv/repo/migrations/20260515184247_create_guilds.exs b/apps/guildhall_ops_db/priv/repo/migrations/20260515184247_create_guilds.exs new file mode 100644 index 0000000..21f709e --- /dev/null +++ b/apps/guildhall_ops_db/priv/repo/migrations/20260515184247_create_guilds.exs @@ -0,0 +1,34 @@ +defmodule Guildhall.OpsDb.Repo.Migrations.CreateGuilds do + use Ecto.Migration + + def change do + create table(:guilds) do + add :guild_id, :integer, null: false + add :name, :string, null: false + add :slug, :string, null: false + add :guild_type, :string, null: false + add :description, :text + add :trust_domain, :string + add :contact_did, :string, null: false + add :registrant_did, :string, null: false + add :registration_ceremony_id, :string + add :status, :string, null: false, default: "pending_approval" + add :enrollment_accord_ref, :string + add :metadata, :map, default: %{} + + timestamps(type: :utc_datetime_usec) + end + + create unique_index(:guilds, [:guild_id]) + create unique_index(:guilds, [:slug]) + create unique_index(:guilds, [:name]) + create index(:guilds, [:status]) + create index(:guilds, [:guild_type]) + create index(:guilds, [:registrant_did]) + + execute( + "ALTER TABLE guilds ADD CONSTRAINT guilds_guild_id_range CHECK (guild_id >= 16 AND guild_id <= 1023)", + "ALTER TABLE guilds DROP CONSTRAINT guilds_guild_id_range" + ) + end +end diff --git a/apps/guildhall_ops_db/priv/repo/migrations/20260515185035_create_guild_schematics.exs b/apps/guildhall_ops_db/priv/repo/migrations/20260515185035_create_guild_schematics.exs new file mode 100644 index 0000000..2f0ab3c --- /dev/null +++ b/apps/guildhall_ops_db/priv/repo/migrations/20260515185035_create_guild_schematics.exs @@ -0,0 +1,24 @@ +defmodule Guildhall.OpsDb.Repo.Migrations.CreateGuildSchematics do + use Ecto.Migration + + def change do + create table(:guild_schematics) do + add :guild_id, references(:guilds, on_delete: :restrict), null: false + add :template_name, :string, null: false + add :schematic_name, :string, null: false + add :schematic_version, :string, null: false + add :tree_hash, :string + add :binding_id, :string + add :realization_id, :string + add :status, :string, null: false, default: "pending" + add :customization_params, :map, default: %{} + add :realization_snapshot, :map, default: %{} + + timestamps(type: :utc_datetime_usec) + end + + create index(:guild_schematics, [:guild_id]) + create index(:guild_schematics, [:status]) + create unique_index(:guild_schematics, [:schematic_name, :schematic_version]) + end +end diff --git a/apps/guildhall_ops_db/priv/repo/migrations/20260515185309_create_guild_memberships.exs b/apps/guildhall_ops_db/priv/repo/migrations/20260515185309_create_guild_memberships.exs new file mode 100644 index 0000000..7fa65e7 --- /dev/null +++ b/apps/guildhall_ops_db/priv/repo/migrations/20260515185309_create_guild_memberships.exs @@ -0,0 +1,27 @@ +defmodule Guildhall.OpsDb.Repo.Migrations.CreateGuildMemberships do + use Ecto.Migration + + def change do + create table(:guild_memberships) do + add :guild_id, references(:guilds, on_delete: :restrict), null: false + add :user_did, :string, null: false + add :user_email, :string, null: false + add :display_name, :string + add :keycloak_sub, :string, null: false + add :role, :string, null: false, default: "apprentice" + add :status, :string, null: false, default: "pending" + add :membership_ceremony_id, :string + add :approved_by_did, :string + add :approved_at, :utc_datetime_usec + add :metadata, :map, default: %{} + + timestamps(type: :utc_datetime_usec) + end + + create unique_index(:guild_memberships, [:guild_id, :user_did]) + create index(:guild_memberships, [:guild_id]) + create index(:guild_memberships, [:user_did]) + create index(:guild_memberships, [:status]) + create index(:guild_memberships, [:keycloak_sub]) + end +end diff --git a/apps/guildhall_orchestrator/lib/guildhall/orchestrator/application.ex b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/application.ex index 49946ba..85639aa 100644 --- a/apps/guildhall_orchestrator/lib/guildhall/orchestrator/application.ex +++ b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/application.ex @@ -5,7 +5,8 @@ defmodule Guildhall.Orchestrator.Application do @impl true def start(_type, _args) do children = [ - Guildhall.Orchestrator.CeremonyOrchestrator + Guildhall.Orchestrator.CeremonyOrchestrator, + Guildhall.Orchestrator.RealizationPoller ] opts = [strategy: :one_for_one, name: Guildhall.Orchestrator.Supervisor] diff --git a/apps/guildhall_orchestrator/lib/guildhall/orchestrator/ceremony_client.ex b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/ceremony_client.ex new file mode 100644 index 0000000..5316fd0 --- /dev/null +++ b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/ceremony_client.ex @@ -0,0 +1,109 @@ +defmodule Guildhall.Orchestrator.CeremonyClient do + @moduledoc false + + alias Ceremony.V1.{ + CeremonyService.Stub, + CreateCeremonyRequest, + ApproveCeremonyRequest, + DenyCeremonyRequest, + GetCeremonyRequest, + ListPendingCeremoniesRequest, + CeremonySubjectMsg + } + + def create_guild_registration_ceremony(guild_name, registrant_did, approver_did) do + request = %CreateCeremonyRequest{ + ceremony_type: "single_approval", + subject: %CeremonySubjectMsg{ + subject_type: "custom", + reference_id: guild_name, + description: "Guild registration: #{guild_name}", + metadata: %{"registrant_did" => registrant_did, "approver_did" => approver_did} + }, + required_approvals: 1, + approver_roles: ["hub_operator"], + ttl_hours: 168 + } + + with {:ok, channel} <- connect(), + {:ok, response} <- Stub.create_ceremony(channel, request) do + GRPC.Stub.disconnect(channel) + + if response.error != "" do + {:error, response.error} + else + {:ok, response} + end + end + end + + def approve_ceremony(ceremony_id, approver_did, role \\ "hub_operator") do + request = %ApproveCeremonyRequest{ + ceremony_id: ceremony_id, + approver_identity: approver_did, + approver_role: role, + comment: "" + } + + with {:ok, channel} <- connect(), + {:ok, response} <- Stub.approve_ceremony(channel, request) do + GRPC.Stub.disconnect(channel) + + if response.error != "" do + {:error, response.error} + else + {:ok, response} + end + end + end + + def deny_ceremony(ceremony_id, approver_did, role, comment) do + request = %DenyCeremonyRequest{ + ceremony_id: ceremony_id, + approver_identity: approver_did, + approver_role: role, + comment: comment + } + + with {:ok, channel} <- connect(), + {:ok, response} <- Stub.deny_ceremony(channel, request) do + GRPC.Stub.disconnect(channel) + + if response.error != "" do + {:error, response.error} + else + {:ok, response} + end + end + end + + def get_ceremony(ceremony_id) do + request = %GetCeremonyRequest{ceremony_id: ceremony_id} + + with {:ok, channel} <- connect(), + {:ok, response} <- Stub.get_ceremony(channel, request) do + GRPC.Stub.disconnect(channel) + + if response.error != "" do + {:error, response.error} + else + {:ok, response} + end + end + end + + def list_pending_ceremonies(intent_id \\ "") do + request = %ListPendingCeremoniesRequest{intent_id: intent_id} + + with {:ok, channel} <- connect(), + {:ok, response} <- Stub.list_pending_ceremonies(channel, request) do + GRPC.Stub.disconnect(channel) + {:ok, response.ceremonies} + end + end + + defp connect do + url = Application.get_env(:guildhall_orchestrator, :ceremony_service_url, "localhost:50053") + GRPC.Stub.connect(url) + end +end diff --git a/apps/guildhall_orchestrator/lib/guildhall/orchestrator/proto/ceremony/v1/ceremony.pb.ex b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/proto/ceremony/v1/ceremony.pb.ex new file mode 100644 index 0000000..8917b52 --- /dev/null +++ b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/proto/ceremony/v1/ceremony.pb.ex @@ -0,0 +1,296 @@ +defmodule Ceremony.V1.CreateCeremonyRequest do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.CreateCeremonyRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ceremony_type, 1, type: :string, json_name: "ceremonyType" + field :subject, 2, type: Ceremony.V1.CeremonySubjectMsg + field :required_approvals, 3, type: :uint32, json_name: "requiredApprovals" + field :approver_roles, 4, repeated: true, type: :string, json_name: "approverRoles" + field :ttl_hours, 5, type: :uint32, json_name: "ttlHours" + field :intent_id, 6, type: :string, json_name: "intentId" + field :run_id, 7, type: :string, json_name: "runId" + field :pr_number, 8, type: :uint64, json_name: "prNumber" + field :remote_name, 9, type: :string, json_name: "remoteName" +end + +defmodule Ceremony.V1.CreateCeremonyResponse do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.CreateCeremonyResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ceremony_id, 1, type: :string, json_name: "ceremonyId" + field :status, 2, type: :string + field :expires_at, 3, type: Google.Protobuf.Timestamp, json_name: "expiresAt" + field :error, 4, type: :string +end + +defmodule Ceremony.V1.ApproveCeremonyRequest do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.ApproveCeremonyRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ceremony_id, 1, type: :string, json_name: "ceremonyId" + field :approver_identity, 2, type: :string, json_name: "approverIdentity" + field :approver_role, 3, type: :string, json_name: "approverRole" + field :comment, 4, type: :string +end + +defmodule Ceremony.V1.ApproveCeremonyResponse do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.ApproveCeremonyResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :success, 1, type: :bool + field :status, 2, type: :string + field :error, 3, type: :string +end + +defmodule Ceremony.V1.DenyCeremonyRequest do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.DenyCeremonyRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ceremony_id, 1, type: :string, json_name: "ceremonyId" + field :approver_identity, 2, type: :string, json_name: "approverIdentity" + field :approver_role, 3, type: :string, json_name: "approverRole" + field :comment, 4, type: :string +end + +defmodule Ceremony.V1.DenyCeremonyResponse do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.DenyCeremonyResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :success, 1, type: :bool + field :status, 2, type: :string + field :error, 3, type: :string +end + +defmodule Ceremony.V1.CancelCeremonyRequest do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.CancelCeremonyRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ceremony_id, 1, type: :string, json_name: "ceremonyId" +end + +defmodule Ceremony.V1.CancelCeremonyResponse do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.CancelCeremonyResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :success, 1, type: :bool + field :error, 2, type: :string +end + +defmodule Ceremony.V1.GetCeremonyRequest do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.GetCeremonyRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ceremony_id, 1, type: :string, json_name: "ceremonyId" +end + +defmodule Ceremony.V1.GetCeremonyResponse do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.GetCeremonyResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ceremony_id, 1, type: :string, json_name: "ceremonyId" + field :ceremony_type, 2, type: :string, json_name: "ceremonyType" + field :subject, 3, type: Ceremony.V1.CeremonySubjectMsg + field :status, 4, type: :string + field :required_approvals, 5, type: :uint32, json_name: "requiredApprovals" + field :current_approvals, 6, type: :uint32, json_name: "currentApprovals" + field :approvals, 7, repeated: true, type: Ceremony.V1.CeremonyApprovalMsg + field :created_at, 8, type: Google.Protobuf.Timestamp, json_name: "createdAt" + field :expires_at, 9, type: Google.Protobuf.Timestamp, json_name: "expiresAt" + field :intent_id, 10, type: :string, json_name: "intentId" + field :run_id, 11, type: :string, json_name: "runId" + field :pr_number, 12, type: :uint64, json_name: "prNumber" + field :remote_name, 13, type: :string, json_name: "remoteName" + field :error, 14, type: :string +end + +defmodule Ceremony.V1.ListPendingCeremoniesRequest do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.ListPendingCeremoniesRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :intent_id, 1, type: :string, json_name: "intentId" +end + +defmodule Ceremony.V1.ListPendingCeremoniesResponse do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.ListPendingCeremoniesResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ceremonies, 1, repeated: true, type: Ceremony.V1.GetCeremonyResponse +end + +defmodule Ceremony.V1.GetCeremonyProofRequest do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.GetCeremonyProofRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ceremony_id, 1, type: :string, json_name: "ceremonyId" +end + +defmodule Ceremony.V1.GetCeremonyProofResponse do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.GetCeremonyProofResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ceremony_id, 1, type: :string, json_name: "ceremonyId" + field :status, 2, type: :string + field :proof_hash, 3, type: :string, json_name: "proofHash" + field :approvals, 4, repeated: true, type: Ceremony.V1.CeremonyApprovalMsg + field :resolved_at, 5, type: Google.Protobuf.Timestamp, json_name: "resolvedAt" + field :error, 6, type: :string +end + +defmodule Ceremony.V1.CheckCeremonyRequirementRequest do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.CheckCeremonyRequirementRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :classification, 1, type: :string +end + +defmodule Ceremony.V1.CheckCeremonyRequirementResponse do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.CheckCeremonyRequirementResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :outcome, 1, type: :string + field :ceremony_type, 2, type: :string, json_name: "ceremonyType" + field :approver_roles, 3, repeated: true, type: :string, json_name: "approverRoles" + field :required_approvals, 4, type: :uint32, json_name: "requiredApprovals" +end + +defmodule Ceremony.V1.CeremonySubjectMsg.MetadataEntry do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.CeremonySubjectMsg.MetadataEntry", + map: true, + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: :string +end + +defmodule Ceremony.V1.CeremonySubjectMsg do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.CeremonySubjectMsg", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :subject_type, 1, type: :string, json_name: "subjectType" + field :reference_id, 2, type: :string, json_name: "referenceId" + field :description, 3, type: :string + + field :metadata, 4, + repeated: true, + type: Ceremony.V1.CeremonySubjectMsg.MetadataEntry, + map: true +end + +defmodule Ceremony.V1.CeremonyApprovalMsg do + @moduledoc false + + use Protobuf, + full_name: "ceremony.v1.CeremonyApprovalMsg", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :approver_identity, 1, type: :string, json_name: "approverIdentity" + field :approver_role, 2, type: :string, json_name: "approverRole" + field :decision, 3, type: :string + field :comment, 4, type: :string + field :decided_at, 5, type: Google.Protobuf.Timestamp, json_name: "decidedAt" +end + +defmodule Ceremony.V1.CeremonyService.Service do + @moduledoc false + + use GRPC.Service, name: "ceremony.v1.CeremonyService", protoc_gen_elixir_version: "0.16.0" + + rpc :CreateCeremony, Ceremony.V1.CreateCeremonyRequest, Ceremony.V1.CreateCeremonyResponse + + rpc :ApproveCeremony, Ceremony.V1.ApproveCeremonyRequest, Ceremony.V1.ApproveCeremonyResponse + + rpc :DenyCeremony, Ceremony.V1.DenyCeremonyRequest, Ceremony.V1.DenyCeremonyResponse + + rpc :CancelCeremony, Ceremony.V1.CancelCeremonyRequest, Ceremony.V1.CancelCeremonyResponse + + rpc :GetCeremony, Ceremony.V1.GetCeremonyRequest, Ceremony.V1.GetCeremonyResponse + + rpc :ListPendingCeremonies, + Ceremony.V1.ListPendingCeremoniesRequest, + Ceremony.V1.ListPendingCeremoniesResponse + + rpc :GetCeremonyProof, Ceremony.V1.GetCeremonyProofRequest, Ceremony.V1.GetCeremonyProofResponse + + rpc :CheckCeremonyRequirement, + Ceremony.V1.CheckCeremonyRequirementRequest, + Ceremony.V1.CheckCeremonyRequirementResponse +end + +defmodule Ceremony.V1.CeremonyService.Stub do + @moduledoc false + + use GRPC.Stub, service: Ceremony.V1.CeremonyService.Service +end diff --git a/apps/guildhall_orchestrator/lib/guildhall/orchestrator/proto/schematic/v1/ffc_schematic.pb.ex b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/proto/schematic/v1/ffc_schematic.pb.ex new file mode 100644 index 0000000..1f18f2b --- /dev/null +++ b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/proto/schematic/v1/ffc_schematic.pb.ex @@ -0,0 +1,1130 @@ +defmodule Schematic.V1.CeremonyType do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.CeremonyType", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :CEREMONY_TYPE_UNSPECIFIED, 0 + field :CEREMONY_TYPE_AUTONOMOUS, 1 + field :CEREMONY_TYPE_SELF_GRANT, 2 + field :CEREMONY_TYPE_SINGLE_APPROVAL, 3 + field :CEREMONY_TYPE_MULTI_PARTY, 4 + field :CEREMONY_TYPE_REFERENDUM, 5 + field :CEREMONY_TYPE_BREAK_GLASS, 6 + field :CEREMONY_TYPE_INHERIT, 7 +end + +defmodule Schematic.V1.AttestationLevel do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.AttestationLevel", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :ATTESTATION_LEVEL_UNSPECIFIED, 0 + field :ATTESTATION_LEVEL_CRYPTOGRAPHIC, 1 + field :ATTESTATION_LEVEL_AUDITABLE, 2 + field :ATTESTATION_LEVEL_INFORMAL, 3 +end + +defmodule Schematic.V1.Channel do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.Channel", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :CHANNEL_UNSPECIFIED, 0 + field :CHANNEL_SHELLSTREAM, 1 + field :CHANNEL_GIT_PR_REVIEW, 2 + field :CHANNEL_JIRA, 3 + field :CHANNEL_SLACK, 4 + field :CHANNEL_EMAIL, 5 + field :CHANNEL_CUSTOM, 6 +end + +defmodule Schematic.V1.ProviderKind do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.ProviderKind", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :PROVIDER_KIND_UNSPECIFIED, 0 + field :PROVIDER_KIND_KEYCLOAK, 1 + field :PROVIDER_KIND_ENTRA, 2 + field :PROVIDER_KIND_OKTA, 3 + field :PROVIDER_KIND_GOOGLE, 4 + field :PROVIDER_KIND_GENERIC_OIDC, 5 +end + +defmodule Schematic.V1.TrustLevel do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.TrustLevel", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :TRUST_LEVEL_UNSPECIFIED, 0 + field :TRUST_LEVEL_FEDERATED, 1 + field :TRUST_LEVEL_HYBRID, 2 + field :TRUST_LEVEL_SOVEREIGN, 3 +end + +defmodule Schematic.V1.ConsortiumRole do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.ConsortiumRole", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :CONSORTIUM_ROLE_UNSPECIFIED, 0 + field :CONSORTIUM_ROLE_FOUNDING_STAKEHOLDER, 1 + field :CONSORTIUM_ROLE_STAKEHOLDER, 2 + field :CONSORTIUM_ROLE_OBSERVER, 3 +end + +defmodule Schematic.V1.TrustTier do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.TrustTier", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :TRUST_TIER_UNSPECIFIED, 0 + field :TRUST_TIER_APPRENTICE, 1 + field :TRUST_TIER_JOURNEYMAN, 2 + field :TRUST_TIER_MASTER, 3 + field :TRUST_TIER_SITE_OWNER, 4 +end + +defmodule Schematic.V1.PeerRelationship do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.PeerRelationship", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :PEER_RELATIONSHIP_UNSPECIFIED, 0 + field :PEER_RELATIONSHIP_TENANT, 1 + field :PEER_RELATIONSHIP_PEER, 2 + field :PEER_RELATIONSHIP_UPSTREAM, 3 +end + +defmodule Schematic.V1.CapabilityCeiling do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.CapabilityCeiling", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :CAPABILITY_CEILING_UNSPECIFIED, 0 + field :CAPABILITY_CEILING_READ, 1 + field :CAPABILITY_CEILING_PROPOSE, 2 + field :CAPABILITY_CEILING_MUTATE, 3 + field :CAPABILITY_CEILING_ADMIN, 4 +end + +defmodule Schematic.V1.FfcRole do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.FfcRole", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :FFC_ROLE_UNSPECIFIED, 0 + field :FFC_ROLE_FOUNDING_STAKEHOLDER, 1 + field :FFC_ROLE_STAKEHOLDER, 2 + field :FFC_ROLE_OBSERVER, 3 +end + +defmodule Schematic.V1.FfcSchematicStatus do + @moduledoc false + + use Protobuf, + enum: true, + full_name: "schematic.v1.FfcSchematicStatus", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :FFC_SCHEMATIC_STATUS_UNSPECIFIED, 0 + field :FFC_SCHEMATIC_STATUS_DRAFT, 1 + field :FFC_SCHEMATIC_STATUS_APPROVED, 2 + field :FFC_SCHEMATIC_STATUS_PUBLISHED, 3 + field :FFC_SCHEMATIC_STATUS_REALIZED, 4 + field :FFC_SCHEMATIC_STATUS_ARCHIVED, 5 + field :FFC_SCHEMATIC_STATUS_WITHDRAWN, 6 +end + +defmodule Schematic.V1.ConsortiumIdentity do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ConsortiumIdentity", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :did, 1, type: :string + field :realm, 2, type: :string + field :description, 3, type: :string +end + +defmodule Schematic.V1.RotationPolicy do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.RotationPolicy", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :svid_ttl, 1, type: :string, json_name: "svidTtl" + field :ca_ttl, 2, type: :string, json_name: "caTtl" +end + +defmodule Schematic.V1.SpiffeConfig do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.SpiffeConfig", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :trust_domain, 1, type: :string, json_name: "trustDomain" + field :spire_server_ref, 2, type: :string, json_name: "spireServerRef" + field :rotation_policy, 3, type: Schematic.V1.RotationPolicy, json_name: "rotationPolicy" +end + +defmodule Schematic.V1.DidConfig do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.DidConfig", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :consortium_did, 1, type: :string, json_name: "consortiumDid" + field :did_method, 2, type: :string, json_name: "didMethod" + field :did_document_location, 3, type: :string, json_name: "didDocumentLocation" + field :key_rotation_policy, 4, type: :string, json_name: "keyRotationPolicy" +end + +defmodule Schematic.V1.AttestationRequirements do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.AttestationRequirements", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :min_state_hash_freshness, 1, type: :string, json_name: "minStateHashFreshness" + field :require_pcr0_through_7, 2, type: :bool, json_name: "requirePcr0Through7" +end + +defmodule Schematic.V1.TrustDomainSpec do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.TrustDomainSpec", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :spiffe, 1, type: Schematic.V1.SpiffeConfig + field :did, 2, type: Schematic.V1.DidConfig + field :attestation_tier, 3, type: :uint32, json_name: "attestationTier" + + field :attestation_requirements, 4, + type: Schematic.V1.AttestationRequirements, + json_name: "attestationRequirements" +end + +defmodule Schematic.V1.OidcConfig do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.OidcConfig", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :issuer, 1, type: :string + field :jwks_uri, 2, type: :string, json_name: "jwksUri" +end + +defmodule Schematic.V1.MfaPolicy do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.MfaPolicy", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :required, 1, type: :bool + field :methods, 2, repeated: true, type: :string +end + +defmodule Schematic.V1.HardwareCredentialPolicy do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.HardwareCredentialPolicy", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :required, 1, type: :bool + field :types, 2, repeated: true, type: :string +end + +defmodule Schematic.V1.GroupRoleMapping do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GroupRoleMapping", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :group, 1, type: :string + field :role, 2, type: :string +end + +defmodule Schematic.V1.IdentityAuthority do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.IdentityAuthority", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :provider, 1, type: Schematic.V1.ProviderKind, enum: true + field :url, 2, type: :string + field :realm, 3, type: :string + field :oidc, 4, type: Schematic.V1.OidcConfig + field :trust_level, 5, type: Schematic.V1.TrustLevel, json_name: "trustLevel", enum: true + field :mfa_policy, 6, type: Schematic.V1.MfaPolicy, json_name: "mfaPolicy" + + field :hardware_credential, 7, + type: Schematic.V1.HardwareCredentialPolicy, + json_name: "hardwareCredential" + + field :group_to_role_mappings, 8, + repeated: true, + type: Schematic.V1.GroupRoleMapping, + json_name: "groupToRoleMappings" +end + +defmodule Schematic.V1.IdentityClaimMapping do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.IdentityClaimMapping", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :did_from_claim, 1, type: :string, json_name: "didFromClaim" + field :roles_from_claim, 2, type: :string, json_name: "rolesFromClaim" +end + +defmodule Schematic.V1.IdentityFederation do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.IdentityFederation", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :upstream_provider, 1, + type: Schematic.V1.ProviderKind, + json_name: "upstreamProvider", + enum: true + + field :upstream_tenant_id, 2, type: :string, json_name: "upstreamTenantId" + field :upstream_issuer, 3, type: :string, json_name: "upstreamIssuer" + field :broker_client_id, 4, type: :string, json_name: "brokerClientId" + field :trust_level, 5, type: Schematic.V1.TrustLevel, json_name: "trustLevel", enum: true + + field :identity_claim_mapping, 6, + type: Schematic.V1.IdentityClaimMapping, + json_name: "identityClaimMapping" +end + +defmodule Schematic.V1.IdentityAuthorityBundle do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.IdentityAuthorityBundle", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :primary, 1, type: Schematic.V1.IdentityAuthority + field :federations, 2, repeated: true, type: Schematic.V1.IdentityFederation +end + +defmodule Schematic.V1.ApprovalChannelSpec do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ApprovalChannelSpec", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :minimum_attestation_level, 1, + type: Schematic.V1.AttestationLevel, + json_name: "minimumAttestationLevel", + enum: true +end + +defmodule Schematic.V1.AllowedChannel do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.AllowedChannel", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :channel, 1, type: Schematic.V1.Channel, enum: true + field :custom_adapter_ref, 2, type: :string, json_name: "customAdapterRef" +end + +defmodule Schematic.V1.RatchetPolicy do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.RatchetPolicy", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :tightening_strategy, 1, + type: Schematic.V1.CeremonyType, + json_name: "tighteningStrategy", + enum: true + + field :loosening_strategy, 2, + type: Schematic.V1.CeremonyType, + json_name: "looseningStrategy", + enum: true +end + +defmodule Schematic.V1.EmergencyOverride do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.EmergencyOverride", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :allowed, 1, type: :bool + field :override_type, 2, type: Schematic.V1.CeremonyType, json_name: "overrideType", enum: true + + field :post_hoc_ceremony, 3, + type: Schematic.V1.CeremonyType, + json_name: "postHocCeremony", + enum: true + + field :post_hoc_window, 4, type: :string, json_name: "postHocWindow" +end + +defmodule Schematic.V1.CeremonyRequirement do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CeremonyRequirement", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + oneof :quorum, 0 + + field :mutation_type, 1, type: :string, json_name: "mutationType" + field :ceremony_type, 2, type: Schematic.V1.CeremonyType, json_name: "ceremonyType", enum: true + field :eligible_roles, 3, repeated: true, type: :string, json_name: "eligibleRoles" + field :quorum_count, 4, type: :uint32, json_name: "quorumCount", oneof: 0 + field :quorum_all, 5, type: :bool, json_name: "quorumAll", oneof: 0 + field :review_window, 6, type: :string, json_name: "reviewWindow" + field :approval_channel, 7, type: Schematic.V1.ApprovalChannelSpec, json_name: "approvalChannel" + + field :allowed_channels, 8, + repeated: true, + type: Schematic.V1.AllowedChannel, + json_name: "allowedChannels" + + field :ratchet, 9, type: Schematic.V1.RatchetPolicy + + field :emergency_override, 10, + type: Schematic.V1.EmergencyOverride, + json_name: "emergencyOverride" +end + +defmodule Schematic.V1.CeremonyRequirementBundle do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CeremonyRequirementBundle", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :entries, 1, repeated: true, type: Schematic.V1.CeremonyRequirement +end + +defmodule Schematic.V1.Member do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.Member", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :identity, 1, type: :string + field :did, 2, type: :string + + field :consortium_role, 3, + type: Schematic.V1.ConsortiumRole, + json_name: "consortiumRole", + enum: true + + field :tier, 4, type: Schematic.V1.TrustTier, enum: true + field :enrolled_at, 5, type: Google.Protobuf.Timestamp, json_name: "enrolledAt" + field :enrollment_ceremony_ref, 6, type: :string, json_name: "enrollmentCeremonyRef" +end + +defmodule Schematic.V1.Members do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.Members", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :members, 1, repeated: true, type: Schematic.V1.Member +end + +defmodule Schematic.V1.SessionGrantPolicy do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.SessionGrantPolicy", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :allowed_roles, 1, repeated: true, type: :string, json_name: "allowedRoles" + + field :capability_ceiling, 2, + type: Schematic.V1.CapabilityCeiling, + json_name: "capabilityCeiling", + enum: true +end + +defmodule Schematic.V1.FederationPeer do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.FederationPeer", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :did, 2, type: :string + field :trust_domain, 3, type: :string, json_name: "trustDomain" + field :relationship, 4, type: Schematic.V1.PeerRelationship, enum: true + field :shared_accord_ref, 5, type: :string, json_name: "sharedAccordRef" + field :mutual_attestation_required, 6, type: :bool, json_name: "mutualAttestationRequired" + + field :session_grant_policy, 7, + type: Schematic.V1.SessionGrantPolicy, + json_name: "sessionGrantPolicy" +end + +defmodule Schematic.V1.FederationPeers do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.FederationPeers", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :peers, 1, repeated: true, type: Schematic.V1.FederationPeer +end + +defmodule Schematic.V1.ComputeNodeRef do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ComputeNodeRef", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :platform_id, 2, type: :string, json_name: "platformId" + field :attestation_tier, 3, type: :uint32, json_name: "attestationTier" + field :registry_ref, 4, type: :string, json_name: "registryRef" + + field :assigned_workload_schematics, 5, + repeated: true, + type: :string, + json_name: "assignedWorkloadSchematics" +end + +defmodule Schematic.V1.ClusterRef do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ClusterRef", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :api_server, 2, type: :string, json_name: "apiServer" + field :spiffe_trust_domain, 3, type: :string, json_name: "spiffeTrustDomain" + field :registry_ref, 4, type: :string, json_name: "registryRef" +end + +defmodule Schematic.V1.BackendRef do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.BackendRef", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :backend_type, 2, type: :string, json_name: "backendType" + field :url, 3, type: :string + field :keycloak_client_id, 4, type: :string, json_name: "keycloakClientId" + field :registry_ref, 5, type: :string, json_name: "registryRef" +end + +defmodule Schematic.V1.CloudProviderRef do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CloudProviderRef", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :provider, 2, type: :string + field :registry_ref, 3, type: :string, json_name: "registryRef" +end + +defmodule Schematic.V1.InfrastructureRefs do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.InfrastructureRefs", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :compute_nodes, 1, + repeated: true, + type: Schematic.V1.ComputeNodeRef, + json_name: "computeNodes" + + field :clusters, 2, repeated: true, type: Schematic.V1.ClusterRef + field :backends, 3, repeated: true, type: Schematic.V1.BackendRef + + field :cloud_providers, 4, + repeated: true, + type: Schematic.V1.CloudProviderRef, + json_name: "cloudProviders" +end + +defmodule Schematic.V1.BootChain do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.BootChain", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :pcr0_through_7, 1, repeated: true, type: :string, json_name: "pcr0Through7" +end + +defmodule Schematic.V1.KernelExpectation do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.KernelExpectation", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :version, 1, type: :string +end + +defmodule Schematic.V1.EbpfProgram do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.EbpfProgram", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :role, 1, type: :string + field :name, 2, type: :string + field :measurement_hash, 3, type: :string, json_name: "measurementHash" +end + +defmodule Schematic.V1.NodeAttestationExpected do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.NodeAttestationExpected", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :boot_chain, 2, type: Schematic.V1.BootChain, json_name: "bootChain" + field :kernel, 3, type: Schematic.V1.KernelExpectation + + field :required_ebpf_programs, 4, + repeated: true, + type: Schematic.V1.EbpfProgram, + json_name: "requiredEbpfPrograms" +end + +defmodule Schematic.V1.GlobalAttestationPolicy do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GlobalAttestationPolicy", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :allow_unexpected_ebpf_programs, 1, type: :bool, json_name: "allowUnexpectedEbpfPrograms" + + field :allow_unexpected_kernel_modules, 2, + type: :bool, + json_name: "allowUnexpectedKernelModules" +end + +defmodule Schematic.V1.ExpectedAttestationProfile do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ExpectedAttestationProfile", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :per_node, 1, + repeated: true, + type: Schematic.V1.NodeAttestationExpected, + json_name: "perNode" + + field :global_policy, 2, type: Schematic.V1.GlobalAttestationPolicy, json_name: "globalPolicy" +end + +defmodule Schematic.V1.FfcSchematicSignature do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.FfcSchematicSignature", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :signer_did, 1, type: :string, json_name: "signerDid" + field :role, 2, type: Schematic.V1.FfcRole, enum: true + field :signature, 3, type: :bytes + field :signed_at, 4, type: Google.Protobuf.Timestamp, json_name: "signedAt" + field :accord_hash, 5, type: :bytes, json_name: "accordHash" + field :key_ref, 6, type: :string, json_name: "keyRef" +end + +defmodule Schematic.V1.FfcSchematicArtifact do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.FfcSchematicArtifact", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :parent_hash, 3, type: :string, json_name: "parentHash" + field :consortium, 4, type: Schematic.V1.ConsortiumIdentity + field :trust_domain, 5, type: Schematic.V1.TrustDomainSpec, json_name: "trustDomain" + field :identity, 6, type: Schematic.V1.IdentityAuthorityBundle + field :ceremonies, 7, type: Schematic.V1.CeremonyRequirementBundle + field :members, 8, type: Schematic.V1.Members + field :federation_peers, 9, type: Schematic.V1.FederationPeers, json_name: "federationPeers" + field :infrastructure, 10, type: Schematic.V1.InfrastructureRefs + field :attestation, 11, type: Schematic.V1.ExpectedAttestationProfile + field :signatures, 12, repeated: true, type: Schematic.V1.FfcSchematicSignature + field :schematic_hash, 13, type: :bytes, json_name: "schematicHash" + field :ceremony_id, 14, type: :bytes, json_name: "ceremonyId" + field :accord_hash, 15, type: :bytes, json_name: "accordHash" + field :created_at, 16, type: Google.Protobuf.Timestamp, json_name: "createdAt" + field :status, 17, type: Schematic.V1.FfcSchematicStatus, enum: true +end + +defmodule Schematic.V1.FfcSchematicFile do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.FfcSchematicFile", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :path, 1, type: :string + field :content, 2, type: :bytes +end + +defmodule Schematic.V1.CreateFfcSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CreateFfcSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :manifest_yaml, 3, type: :bytes, json_name: "manifestYaml" + field :files, 4, repeated: true, type: Schematic.V1.FfcSchematicFile +end + +defmodule Schematic.V1.CreateFfcSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CreateFfcSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :tree_hash, 3, type: :string, json_name: "treeHash" + field :status, 4, type: Schematic.V1.FfcSchematicStatus, enum: true +end + +defmodule Schematic.V1.GetFfcSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GetFfcSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string +end + +defmodule Schematic.V1.GetFfcSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GetFfcSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :artifact, 1, type: Schematic.V1.FfcSchematicArtifact +end + +defmodule Schematic.V1.ListFfcSchematicVersionsRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ListFfcSchematicVersionsRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string +end + +defmodule Schematic.V1.FfcVersionInfo do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.FfcVersionInfo", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :version, 1, type: :string + field :parent_hash, 2, type: :string, json_name: "parentHash" + field :status, 3, type: Schematic.V1.FfcSchematicStatus, enum: true + field :created_at, 4, type: Google.Protobuf.Timestamp, json_name: "createdAt" +end + +defmodule Schematic.V1.ListFfcSchematicVersionsResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ListFfcSchematicVersionsResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :versions, 1, repeated: true, type: Schematic.V1.FfcVersionInfo +end + +defmodule Schematic.V1.UpdateFfcSchematicDraftRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.UpdateFfcSchematicDraftRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :files, 3, repeated: true, type: Schematic.V1.FfcSchematicFile +end + +defmodule Schematic.V1.UpdateFfcSchematicDraftResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.UpdateFfcSchematicDraftResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :tree_hash, 1, type: :string, json_name: "treeHash" +end + +defmodule Schematic.V1.ValidateFfcSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ValidateFfcSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string +end + +defmodule Schematic.V1.FfcValidationResult do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.FfcValidationResult", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :validator, 1, type: :string + field :passed, 2, type: :bool + field :message, 3, type: :string + field :severity, 4, type: :string +end + +defmodule Schematic.V1.ValidateFfcSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ValidateFfcSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :valid, 1, type: :bool + field :results, 2, repeated: true, type: Schematic.V1.FfcValidationResult +end + +defmodule Schematic.V1.ApproveFfcSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ApproveFfcSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :signer_did, 3, type: :string, json_name: "signerDid" + field :role, 4, type: Schematic.V1.FfcRole, enum: true + field :signature, 5, type: :bytes + field :accord_hash, 6, type: :bytes, json_name: "accordHash" + field :key_ref, 7, type: :string, json_name: "keyRef" +end + +defmodule Schematic.V1.ApproveFfcSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ApproveFfcSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :accepted, 1, type: :bool + field :message, 2, type: :string +end + +defmodule Schematic.V1.PublishFfcSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.PublishFfcSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string +end + +defmodule Schematic.V1.PublishFfcSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.PublishFfcSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :schematic_hash, 1, type: :bytes, json_name: "schematicHash" + field :ceremony_id, 2, type: :bytes, json_name: "ceremonyId" + field :status, 3, type: Schematic.V1.FfcSchematicStatus, enum: true +end + +defmodule Schematic.V1.RealizeFfcSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.RealizeFfcSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string +end + +defmodule Schematic.V1.RealizeFfcSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.RealizeFfcSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :realization_id, 1, type: :string, json_name: "realizationId" + field :status, 2, type: Schematic.V1.FfcSchematicStatus, enum: true +end + +defmodule Schematic.V1.GetRealizationStatusRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GetRealizationStatusRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :realization_id, 1, type: :string, json_name: "realizationId" +end + +defmodule Schematic.V1.FfcSectionRealizationStatus do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.FfcSectionRealizationStatus", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :section_name, 1, type: :string, json_name: "sectionName" + field :status, 2, type: :string + field :message, 3, type: :string +end + +defmodule Schematic.V1.GetRealizationStatusResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GetRealizationStatusResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :overall_status, 1, + type: Schematic.V1.FfcSchematicStatus, + json_name: "overallStatus", + enum: true + + field :per_section, 2, + repeated: true, + type: Schematic.V1.FfcSectionRealizationStatus, + json_name: "perSection" +end + +defmodule Schematic.V1.CreateNextFfcVersionRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CreateNextFfcVersionRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :from_version, 2, type: :string, json_name: "fromVersion" + field :bump, 3, type: :string +end + +defmodule Schematic.V1.CreateNextFfcVersionResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CreateNextFfcVersionResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :new_version, 1, type: :string, json_name: "newVersion" + field :parent_hash, 2, type: :string, json_name: "parentHash" +end + +defmodule Schematic.V1.FfcSchematicService.Service do + @moduledoc false + + use GRPC.Service, name: "schematic.v1.FfcSchematicService", protoc_gen_elixir_version: "0.16.0" + + rpc :CreateFfcSchematic, + Schematic.V1.CreateFfcSchematicRequest, + Schematic.V1.CreateFfcSchematicResponse + + rpc :GetFfcSchematic, Schematic.V1.GetFfcSchematicRequest, Schematic.V1.GetFfcSchematicResponse + + rpc :ListFfcSchematicVersions, + Schematic.V1.ListFfcSchematicVersionsRequest, + Schematic.V1.ListFfcSchematicVersionsResponse + + rpc :UpdateFfcSchematicDraft, + Schematic.V1.UpdateFfcSchematicDraftRequest, + Schematic.V1.UpdateFfcSchematicDraftResponse + + rpc :ValidateFfcSchematic, + Schematic.V1.ValidateFfcSchematicRequest, + Schematic.V1.ValidateFfcSchematicResponse + + rpc :ApproveFfcSchematic, + Schematic.V1.ApproveFfcSchematicRequest, + Schematic.V1.ApproveFfcSchematicResponse + + rpc :PublishFfcSchematic, + Schematic.V1.PublishFfcSchematicRequest, + Schematic.V1.PublishFfcSchematicResponse + + rpc :RealizeFfcSchematic, + Schematic.V1.RealizeFfcSchematicRequest, + Schematic.V1.RealizeFfcSchematicResponse + + rpc :GetRealizationStatus, + Schematic.V1.GetRealizationStatusRequest, + Schematic.V1.GetRealizationStatusResponse + + rpc :CreateNextVersion, + Schematic.V1.CreateNextFfcVersionRequest, + Schematic.V1.CreateNextFfcVersionResponse +end + +defmodule Schematic.V1.FfcSchematicService.Stub do + @moduledoc false + + use GRPC.Stub, service: Schematic.V1.FfcSchematicService.Service +end diff --git a/apps/guildhall_orchestrator/lib/guildhall/orchestrator/proto/schematic/v1/schematics.pb.ex b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/proto/schematic/v1/schematics.pb.ex new file mode 100644 index 0000000..8bc0c93 --- /dev/null +++ b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/proto/schematic/v1/schematics.pb.ex @@ -0,0 +1,452 @@ +defmodule Schematic.V1.CreateSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CreateSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :manifest_yaml, 3, type: :bytes, json_name: "manifestYaml" + field :files, 4, repeated: true, type: Schematic.V1.SchematicFile +end + +defmodule Schematic.V1.SchematicFile do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.SchematicFile", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :path, 1, type: :string + field :content, 2, type: :bytes +end + +defmodule Schematic.V1.CreateSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CreateSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :tree_hash, 3, type: :string, json_name: "treeHash" + field :status, 4, type: :string +end + +defmodule Schematic.V1.GetSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GetSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string +end + +defmodule Schematic.V1.GetSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GetSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :tree_hash, 3, type: :string, json_name: "treeHash" + field :status, 4, type: :string + field :parent_hash, 5, type: :string, json_name: "parentHash" + field :stakeholders, 6, repeated: true, type: Schematic.V1.StakeholderInfo + field :created_at, 7, type: Google.Protobuf.Timestamp, json_name: "createdAt" +end + +defmodule Schematic.V1.StakeholderInfo do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.StakeholderInfo", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :role, 1, type: :string + field :identity, 2, type: :string + field :required, 3, type: :bool +end + +defmodule Schematic.V1.ListVersionsRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ListVersionsRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string +end + +defmodule Schematic.V1.ListVersionsResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ListVersionsResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :versions, 1, repeated: true, type: Schematic.V1.VersionInfo +end + +defmodule Schematic.V1.VersionInfo do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.VersionInfo", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :version, 1, type: :string + field :tree_hash, 2, type: :string, json_name: "treeHash" + field :status, 3, type: :string + field :parent_hash, 4, type: :string, json_name: "parentHash" + field :created_at, 5, type: Google.Protobuf.Timestamp, json_name: "createdAt" +end + +defmodule Schematic.V1.UpdateSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.UpdateSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :files, 3, repeated: true, type: Schematic.V1.SchematicFile +end + +defmodule Schematic.V1.UpdateSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.UpdateSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :tree_hash, 1, type: :string, json_name: "treeHash" + field :status, 2, type: :string +end + +defmodule Schematic.V1.ValidateSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ValidateSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string +end + +defmodule Schematic.V1.ValidateSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ValidateSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :valid, 1, type: :bool + field :error_count, 2, type: :int32, json_name: "errorCount" + field :warning_count, 3, type: :int32, json_name: "warningCount" + field :results, 4, repeated: true, type: Schematic.V1.ValidationResultProto +end + +defmodule Schematic.V1.ValidationResultProto do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ValidationResultProto", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :validator, 1, type: :string + field :passed, 2, type: :bool + field :message, 3, type: :string + field :severity, 4, type: :string +end + +defmodule Schematic.V1.ApproveSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ApproveSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :role, 3, type: :string + field :identity, 4, type: :string + field :tree_hash, 5, type: :string, json_name: "treeHash" + field :comment, 6, type: :string +end + +defmodule Schematic.V1.ApproveSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ApproveSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :accepted, 1, type: :bool + field :approval_status, 2, type: :string, json_name: "approvalStatus" + field :message, 3, type: :string +end + +defmodule Schematic.V1.GetApprovalStatusRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GetApprovalStatusRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string +end + +defmodule Schematic.V1.GetApprovalStatusResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GetApprovalStatusResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :status, 1, type: :string + field :approved_roles, 2, repeated: true, type: :string, json_name: "approvedRoles" + field :remaining_roles, 3, repeated: true, type: :string, json_name: "remainingRoles" + field :approvals, 4, repeated: true, type: Schematic.V1.ApprovalInfo +end + +defmodule Schematic.V1.ApprovalInfo do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ApprovalInfo", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :role, 1, type: :string + field :identity, 2, type: :string + field :approved_at, 3, type: Google.Protobuf.Timestamp, json_name: "approvedAt" + field :comment, 4, type: :string +end + +defmodule Schematic.V1.PublishSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.PublishSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string +end + +defmodule Schematic.V1.PublishSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.PublishSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :tree_hash, 1, type: :string, json_name: "treeHash" + field :status, 2, type: :string +end + +defmodule Schematic.V1.CreateNextVersionRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CreateNextVersionRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :from_version, 2, type: :string, json_name: "fromVersion" + field :bump, 3, type: :string +end + +defmodule Schematic.V1.CreateNextVersionResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CreateNextVersionResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :version, 1, type: :string + field :tree_hash, 2, type: :string, json_name: "treeHash" + field :parent_hash, 3, type: :string, json_name: "parentHash" + field :status, 4, type: :string +end + +defmodule Schematic.V1.ForkSchematicRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ForkSchematicRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :source_name, 1, type: :string, json_name: "sourceName" + field :source_version, 2, type: :string, json_name: "sourceVersion" + field :new_name, 3, type: :string, json_name: "newName" + field :new_version, 4, type: :string, json_name: "newVersion" + field :operations, 5, repeated: true, type: Schematic.V1.TemplateOperation +end + +defmodule Schematic.V1.TemplateOperation do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.TemplateOperation", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :op_type, 1, type: :string, json_name: "opType" + field :path, 2, type: :string + field :content, 3, type: :bytes +end + +defmodule Schematic.V1.ForkSchematicResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.ForkSchematicResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :name, 1, type: :string + field :version, 2, type: :string + field :tree_hash, 3, type: :string, json_name: "treeHash" + field :status, 4, type: :string +end + +defmodule Schematic.V1.CreateDeploymentBindingRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CreateDeploymentBindingRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :schematic_name, 1, type: :string, json_name: "schematicName" + field :schematic_version, 2, type: :string, json_name: "schematicVersion" + field :pipeline_name, 3, type: :string, json_name: "pipelineName" + field :target_env, 4, type: :string, json_name: "targetEnv" +end + +defmodule Schematic.V1.CreateDeploymentBindingResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.CreateDeploymentBindingResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :binding_id, 1, type: :string, json_name: "bindingId" + field :status, 2, type: :string +end + +defmodule Schematic.V1.GetDeploymentBindingRequest do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GetDeploymentBindingRequest", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :binding_id, 1, type: :string, json_name: "bindingId" +end + +defmodule Schematic.V1.GetDeploymentBindingResponse do + @moduledoc false + + use Protobuf, + full_name: "schematic.v1.GetDeploymentBindingResponse", + protoc_gen_elixir_version: "0.16.0", + syntax: :proto3 + + field :binding_id, 1, type: :string, json_name: "bindingId" + field :schematic_name, 2, type: :string, json_name: "schematicName" + field :schematic_version, 3, type: :string, json_name: "schematicVersion" + field :tree_hash, 4, type: :string, json_name: "treeHash" + field :pipeline_name, 5, type: :string, json_name: "pipelineName" + field :target_env, 6, type: :string, json_name: "targetEnv" + field :status, 7, type: :string + field :run_id, 8, type: :string, json_name: "runId" + field :created_at, 9, type: Google.Protobuf.Timestamp, json_name: "createdAt" + field :deployed_at, 10, type: Google.Protobuf.Timestamp, json_name: "deployedAt" +end + +defmodule Schematic.V1.SchematicsService.Service do + @moduledoc false + + use GRPC.Service, name: "schematic.v1.SchematicsService", protoc_gen_elixir_version: "0.16.0" + + rpc :CreateSchematic, Schematic.V1.CreateSchematicRequest, Schematic.V1.CreateSchematicResponse + + rpc :GetSchematic, Schematic.V1.GetSchematicRequest, Schematic.V1.GetSchematicResponse + + rpc :ListVersions, Schematic.V1.ListVersionsRequest, Schematic.V1.ListVersionsResponse + + rpc :UpdateSchematic, Schematic.V1.UpdateSchematicRequest, Schematic.V1.UpdateSchematicResponse + + rpc :ValidateSchematic, + Schematic.V1.ValidateSchematicRequest, + Schematic.V1.ValidateSchematicResponse + + rpc :ApproveSchematic, + Schematic.V1.ApproveSchematicRequest, + Schematic.V1.ApproveSchematicResponse + + rpc :GetApprovalStatus, + Schematic.V1.GetApprovalStatusRequest, + Schematic.V1.GetApprovalStatusResponse + + rpc :PublishSchematic, + Schematic.V1.PublishSchematicRequest, + Schematic.V1.PublishSchematicResponse + + rpc :CreateNextVersion, + Schematic.V1.CreateNextVersionRequest, + Schematic.V1.CreateNextVersionResponse + + rpc :ForkSchematic, Schematic.V1.ForkSchematicRequest, Schematic.V1.ForkSchematicResponse + + rpc :CreateDeploymentBinding, + Schematic.V1.CreateDeploymentBindingRequest, + Schematic.V1.CreateDeploymentBindingResponse + + rpc :GetDeploymentBinding, + Schematic.V1.GetDeploymentBindingRequest, + Schematic.V1.GetDeploymentBindingResponse +end + +defmodule Schematic.V1.SchematicsService.Stub do + @moduledoc false + + use GRPC.Stub, service: Schematic.V1.SchematicsService.Service +end diff --git a/apps/guildhall_orchestrator/lib/guildhall/orchestrator/realization_poller.ex b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/realization_poller.ex new file mode 100644 index 0000000..938843d --- /dev/null +++ b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/realization_poller.ex @@ -0,0 +1,91 @@ +defmodule Guildhall.Orchestrator.RealizationPoller do + use GenServer + require Logger + + alias Guildhall.Orchestrator.SchematicClient + + @poll_interval_ms 5_000 + + def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__) + + def watch(realization_id, guild_slug) do + GenServer.cast(__MODULE__, {:watch, realization_id, guild_slug}) + end + + def unwatch(realization_id) do + GenServer.cast(__MODULE__, {:unwatch, realization_id}) + end + + @impl true + def init(_opts) do + Logger.info("RealizationPoller started") + schedule_poll() + {:ok, %{watches: %{}}} + end + + @impl true + def handle_cast({:watch, realization_id, guild_slug}, state) do + {:noreply, put_in(state, [:watches, realization_id], guild_slug)} + end + + @impl true + def handle_cast({:unwatch, realization_id}, state) do + {:noreply, %{state | watches: Map.delete(state.watches, realization_id)}} + end + + @impl true + def handle_info(:poll, %{watches: watches} = state) when map_size(watches) == 0 do + schedule_poll() + {:noreply, state} + end + + @impl true + def handle_info(:poll, state) do + new_watches = + Enum.reduce(state.watches, state.watches, fn {realization_id, guild_slug}, acc -> + case SchematicClient.get_realization_status(realization_id) do + {:ok, response} -> + snapshot = build_snapshot(response) + + Phoenix.PubSub.broadcast( + Guildhall.PubSub, + "realization:#{guild_slug}", + {:realization_update, snapshot} + ) + + if terminal_status?(response.overall_status) do + Map.delete(acc, realization_id) + else + acc + end + + {:error, reason} -> + Logger.warning("Realization poll failed for #{realization_id}: #{inspect(reason)}") + acc + end + end) + + schedule_poll() + {:noreply, %{state | watches: new_watches}} + end + + @impl true + def handle_info(_msg, state), do: {:noreply, state} + + defp schedule_poll, do: Process.send_after(self(), :poll, @poll_interval_ms) + + defp build_snapshot(response) do + %{ + overall_status: to_string(response.overall_status), + sections: + Enum.map(response.per_section, fn s -> + %{name: s.section_name, status: s.status, message: s.message} + end) + } + end + + defp terminal_status?(:FFC_SCHEMATIC_STATUS_REALIZED), do: true + defp terminal_status?(:FFC_SCHEMATIC_STATUS_ARCHIVED), do: true + defp terminal_status?(:FFC_SCHEMATIC_STATUS_WITHDRAWN), do: true + defp terminal_status?(_), do: false +end diff --git a/apps/guildhall_orchestrator/lib/guildhall/orchestrator/schematic_client.ex b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/schematic_client.ex new file mode 100644 index 0000000..2b7aaf8 --- /dev/null +++ b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/schematic_client.ex @@ -0,0 +1,98 @@ +defmodule Guildhall.Orchestrator.SchematicClient do + @moduledoc false + + alias Schematic.V1.{ + SchematicsService.Stub, + ForkSchematicRequest, + TemplateOperation, + CreateDeploymentBindingRequest, + GetDeploymentBindingRequest + } + + alias Schematic.V1.RealizeFfcSchematicRequest + alias Schematic.V1.GetRealizationStatusRequest + + def fork_schematic(source_name, source_version, new_name, new_version, operations) do + ops = + Enum.map(operations, fn op -> + %TemplateOperation{ + op_type: op[:op_type] || "replace", + path: op[:path], + content: op[:content] || "" + } + end) + + request = %ForkSchematicRequest{ + source_name: source_name, + source_version: source_version, + new_name: new_name, + new_version: new_version, + operations: ops + } + + with {:ok, channel} <- connect_schematic(), + {:ok, response} <- Stub.fork_schematic(channel, request) do + GRPC.Stub.disconnect(channel) + {:ok, response} + end + end + + def create_deployment_binding(schematic_name, version, pipeline_name, target_env) do + request = %CreateDeploymentBindingRequest{ + schematic_name: schematic_name, + schematic_version: version, + pipeline_name: pipeline_name, + target_env: target_env + } + + with {:ok, channel} <- connect_schematic(), + {:ok, response} <- Stub.create_deployment_binding(channel, request) do + GRPC.Stub.disconnect(channel) + {:ok, response} + end + end + + def get_deployment_binding(binding_id) do + request = %GetDeploymentBindingRequest{binding_id: binding_id} + + with {:ok, channel} <- connect_schematic(), + {:ok, response} <- Stub.get_deployment_binding(channel, request) do + GRPC.Stub.disconnect(channel) + {:ok, response} + end + end + + def realize_ffc_schematic(name, version) do + request = %RealizeFfcSchematicRequest{name: name, version: version} + + with {:ok, channel} <- connect_ffc(), + {:ok, response} <- Schematic.V1.FfcSchematicService.Stub.realize_ffc_schematic(channel, request) do + GRPC.Stub.disconnect(channel) + {:ok, response} + end + end + + def get_realization_status(realization_id) do + request = %GetRealizationStatusRequest{realization_id: realization_id} + + with {:ok, channel} <- connect_ffc(), + {:ok, response} <- Schematic.V1.FfcSchematicService.Stub.get_realization_status(channel, request) do + GRPC.Stub.disconnect(channel) + {:ok, response} + end + end + + defp connect_schematic do + url = + Application.get_env(:guildhall_orchestrator, :schematic_service_url, "localhost:9091") + + GRPC.Stub.connect(url) + end + + defp connect_ffc do + url = + Application.get_env(:guildhall_orchestrator, :ffc_schematic_service_url, "localhost:9091") + + GRPC.Stub.connect(url) + end +end diff --git a/apps/guildhall_orchestrator/lib/guildhall/orchestrator/schematic_template.ex b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/schematic_template.ex new file mode 100644 index 0000000..bb9f8c3 --- /dev/null +++ b/apps/guildhall_orchestrator/lib/guildhall/orchestrator/schematic_template.ex @@ -0,0 +1,70 @@ +defmodule Guildhall.Orchestrator.SchematicTemplate do + @moduledoc false + + @type_to_template %{ + "msp" => "msp-founding.toml", + "isv" => "isv-founding.toml", + "nsp" => "nsp-founding.toml" + } + + def load_template(guild_type) do + filename = Map.fetch!(@type_to_template, guild_type) + path = Path.join(template_dir(), filename) + + case Toml.decode_file(path) do + {:ok, data} -> {:ok, data} + {:error, reason} -> {:error, {:template_load_failed, reason}} + end + end + + def render_template(template, params) when is_map(template) and is_map(params) do + deep_substitute(template, params) + end + + def to_fork_operations(rendered_template) do + rendered_template + |> Map.drop(["meta"]) + |> Enum.flat_map(fn {section, content} -> + yaml = to_yaml_string(content) + + [ + %{ + op_type: "replace", + path: "#{section}.yaml", + content: yaml + } + ] + end) + end + + def source_schematic(template) do + meta = Map.get(template, "meta", %{}) + {Map.get(meta, "source_schematic", ""), Map.get(meta, "source_version", "1.0.0")} + end + + defp deep_substitute(value, params) when is_binary(value) do + Enum.reduce(params, value, fn {key, val}, acc -> + String.replace(acc, "{{#{key}}}", to_string(val)) + end) + end + + defp deep_substitute(value, params) when is_map(value) do + Map.new(value, fn {k, v} -> {k, deep_substitute(v, params)} end) + end + + defp deep_substitute(value, params) when is_list(value) do + Enum.map(value, &deep_substitute(&1, params)) + end + + defp deep_substitute(value, _params), do: value + + defp to_yaml_string(map) when is_map(map) do + Jason.encode!(map) + end + + defp to_yaml_string(other), do: to_string(other) + + defp template_dir do + Application.app_dir(:guildhall_orchestrator, "priv/schematic_templates") + end +end diff --git a/apps/guildhall_orchestrator/mix.exs b/apps/guildhall_orchestrator/mix.exs index 9860875..4b522e3 100644 --- a/apps/guildhall_orchestrator/mix.exs +++ b/apps/guildhall_orchestrator/mix.exs @@ -28,7 +28,11 @@ defmodule Guildhall.Orchestrator.MixProject do [ {:guildhall_ops_db, in_umbrella: true}, {:phoenix_pubsub, "~> 2.1"}, - {:jason, "~> 1.4"} + {:jason, "~> 1.4"}, + {:grpc, "~> 0.9"}, + {:protobuf, "~> 0.13"}, + {:google_protos, "~> 0.4"}, + {:toml, "~> 0.7"} ] end end diff --git a/apps/guildhall_orchestrator/priv/schematic_templates/isv-founding.toml b/apps/guildhall_orchestrator/priv/schematic_templates/isv-founding.toml new file mode 100644 index 0000000..5dd8340 --- /dev/null +++ b/apps/guildhall_orchestrator/priv/schematic_templates/isv-founding.toml @@ -0,0 +1,36 @@ +[meta] +template_name = "isv-founding" +description = "Independent Software Vendor founding schematic" +source_schematic = "guildhouse-isv-base" +source_version = "1.0.0" + +[consortium] +realm = "{{guild_slug}}" +description = "{{guild_name}} ISV Consortium" + +[trust_domain] +spiffe_trust_domain = "{{trust_domain}}" +attestation_tier = 1 + +[identity_authority] +provider = "keycloak" +url = "https://auth.guildhouse.dev" +realm = "{{guild_slug}}" +trust_level = "federated" +mfa_required = false + +[ceremonies.code_change] +type = "single_approval" +eligible_roles = ["master", "journeyman"] +quorum = 1 + +[ceremonies.governance_change] +type = "single_approval" +eligible_roles = ["master"] +quorum = 1 + +[members] +initial_roles = ["master"] + +[infrastructure] +compute_attestation_tier = 1 diff --git a/apps/guildhall_orchestrator/priv/schematic_templates/msp-founding.toml b/apps/guildhall_orchestrator/priv/schematic_templates/msp-founding.toml new file mode 100644 index 0000000..7671b6c --- /dev/null +++ b/apps/guildhall_orchestrator/priv/schematic_templates/msp-founding.toml @@ -0,0 +1,36 @@ +[meta] +template_name = "msp-founding" +description = "Managed Service Provider founding schematic" +source_schematic = "guildhouse-msp-base" +source_version = "1.0.0" + +[consortium] +realm = "{{guild_slug}}" +description = "{{guild_name}} MSP Consortium" + +[trust_domain] +spiffe_trust_domain = "{{trust_domain}}" +attestation_tier = 2 + +[identity_authority] +provider = "keycloak" +url = "https://auth.guildhouse.dev" +realm = "{{guild_slug}}" +trust_level = "federated" +mfa_required = true + +[ceremonies.code_change] +type = "single_approval" +eligible_roles = ["master", "journeyman"] +quorum = 1 + +[ceremonies.governance_change] +type = "single_approval" +eligible_roles = ["master"] +quorum = 1 + +[members] +initial_roles = ["master"] + +[infrastructure] +compute_attestation_tier = 2 diff --git a/apps/guildhall_orchestrator/priv/schematic_templates/nsp-founding.toml b/apps/guildhall_orchestrator/priv/schematic_templates/nsp-founding.toml new file mode 100644 index 0000000..a08628a --- /dev/null +++ b/apps/guildhall_orchestrator/priv/schematic_templates/nsp-founding.toml @@ -0,0 +1,39 @@ +[meta] +template_name = "nsp-founding" +description = "Network Service Provider founding schematic" +source_schematic = "guildhouse-nsp-base" +source_version = "1.0.0" + +[consortium] +realm = "{{guild_slug}}" +description = "{{guild_name}} NSP Consortium" + +[trust_domain] +spiffe_trust_domain = "{{trust_domain}}" +attestation_tier = 3 + +[identity_authority] +provider = "keycloak" +url = "https://auth.guildhouse.dev" +realm = "{{guild_slug}}" +trust_level = "federated" +mfa_required = true +hardware_credential_required = true + +[ceremonies.code_change] +type = "single_approval" +eligible_roles = ["master", "journeyman"] +quorum = 1 + +[ceremonies.governance_change] +type = "multi_party" +eligible_roles = ["master"] +quorum = 2 + +[members] +initial_roles = ["master"] + +[infrastructure] +compute_attestation_tier = 3 +wireguard_tunnel = true +vpp_dataplane = true diff --git a/apps/guildhall_web/lib/guildhall_web/application.ex b/apps/guildhall_web/lib/guildhall_web/application.ex index f192d36..7701c05 100644 --- a/apps/guildhall_web/lib/guildhall_web/application.ex +++ b/apps/guildhall_web/lib/guildhall_web/application.ex @@ -7,15 +7,31 @@ defmodule Guildhall.Application do @impl true def start(_type, _args) do - children = [ - GuildhallWeb.Telemetry, - {DNSCluster, query: Application.get_env(:guildhall_web, :dns_cluster_query) || :ignore}, - {Phoenix.PubSub, name: Guildhall.PubSub}, - # Start a worker by calling: Guildhall.Worker.start_link(arg) - # {Guildhall.Worker, arg}, - # Start to serve requests, typically the last entry - GuildhallWeb.Endpoint - ] + oidc_children = + case Application.get_env(:guildhall_web, :oidc) do + nil -> + [] + + config -> + [ + {Oidcc.ProviderConfiguration.Worker, + %{ + issuer: config[:issuer], + name: GuildhallWeb.OidcProvider + }} + ] + end + + children = + [ + GuildhallWeb.Telemetry, + {DNSCluster, query: Application.get_env(:guildhall_web, :dns_cluster_query) || :ignore}, + {Phoenix.PubSub, name: Guildhall.PubSub} + ] ++ + oidc_children ++ + [ + GuildhallWeb.Endpoint + ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options diff --git a/apps/guildhall_web/lib/guildhall_web_web/controllers/auth_controller.ex b/apps/guildhall_web/lib/guildhall_web_web/controllers/auth_controller.ex new file mode 100644 index 0000000..ac867e8 --- /dev/null +++ b/apps/guildhall_web/lib/guildhall_web_web/controllers/auth_controller.ex @@ -0,0 +1,96 @@ +defmodule GuildhallWeb.AuthController do + use GuildhallWeb, :controller + + @provider_name GuildhallWeb.OidcProvider + + def login(conn, _params) do + oidc_config = Application.fetch_env!(:guildhall_web, :oidc) + client_id = oidc_config[:client_id] + client_secret = oidc_config[:client_secret] + redirect_uri = oidc_config[:redirect_uri] + + nonce = :crypto.strong_rand_bytes(16) |> Base.url_encode64(padding: false) + state = :crypto.strong_rand_bytes(16) |> Base.url_encode64(padding: false) + + case Oidcc.create_redirect_url(@provider_name, client_id, client_secret, %{ + redirect_uri: redirect_uri, + scopes: ["openid", "profile", "email"], + state: state, + nonce: nonce + }) do + {:ok, url} -> + conn + |> put_session(:oidc_state, state) + |> put_session(:oidc_nonce, nonce) + |> redirect(external: url) + + {:error, reason} -> + conn + |> put_flash(:error, "Failed to initiate login: #{inspect(reason)}") + |> redirect(to: "/") + end + end + + def callback(conn, %{"code" => code, "state" => state}) do + saved_state = get_session(conn, :oidc_state) + + if state != saved_state do + conn + |> put_flash(:error, "Invalid state parameter.") + |> redirect(to: "/") + else + oidc_config = Application.fetch_env!(:guildhall_web, :oidc) + client_id = oidc_config[:client_id] + client_secret = oidc_config[:client_secret] + redirect_uri = oidc_config[:redirect_uri] + nonce = get_session(conn, :oidc_nonce) + + case Oidcc.retrieve_token(code, @provider_name, client_id, client_secret, %{ + redirect_uri: redirect_uri, + nonce: nonce + }) do + {:ok, token} -> + claims = extract_claims(token) + + preferred_username = + claims["preferred_username"] || claims["email"] || claims["sub"] + + current_user = %{ + "sub" => claims["sub"], + "email" => claims["email"], + "name" => claims["name"] || preferred_username, + "preferred_username" => preferred_username, + "did" => "did:web:guildhouse.dev:user:#{preferred_username}" + } + + conn + |> delete_session(:oidc_state) + |> delete_session(:oidc_nonce) + |> put_session(:current_user, current_user) + |> put_flash(:info, "Welcome, #{current_user["name"]}.") + |> redirect(to: "/") + + {:error, reason} -> + conn + |> put_flash(:error, "Authentication failed: #{inspect(reason)}") + |> redirect(to: "/") + end + end + end + + def callback(conn, _params) do + conn + |> put_flash(:error, "Missing authorization code.") + |> redirect(to: "/") + end + + def logout(conn, _params) do + conn + |> configure_session(drop: true) + |> put_flash(:info, "Signed out.") + |> redirect(to: "/") + end + + defp extract_claims(%Oidcc.Token{id: %Oidcc.Token.Id{claims: claims}}), do: claims + defp extract_claims(_), do: %{} +end diff --git a/apps/guildhall_web/lib/guildhall_web_web/live/auth_hooks.ex b/apps/guildhall_web/lib/guildhall_web_web/live/auth_hooks.ex new file mode 100644 index 0000000..922d6da --- /dev/null +++ b/apps/guildhall_web/lib/guildhall_web_web/live/auth_hooks.ex @@ -0,0 +1,16 @@ +defmodule GuildhallWeb.AuthHooks do + @moduledoc false + import Phoenix.LiveView + import Phoenix.Component + + def on_mount(:require_auth, _params, session, socket) do + case session["current_user"] do + nil -> {:halt, redirect(socket, to: "/auth/login")} + user -> {:cont, assign(socket, :current_user, user)} + end + end + + def on_mount(:fetch_user, _params, session, socket) do + {:cont, assign(socket, :current_user, session["current_user"])} + end +end diff --git a/apps/guildhall_web/lib/guildhall_web_web/live/dashboard_live.ex b/apps/guildhall_web/lib/guildhall_web_web/live/dashboard_live.ex index d8fbf1e..cb54b37 100644 --- a/apps/guildhall_web/lib/guildhall_web_web/live/dashboard_live.ex +++ b/apps/guildhall_web/lib/guildhall_web_web/live/dashboard_live.ex @@ -5,7 +5,7 @@ defmodule GuildhallWeb.DashboardLive do """ use GuildhallWeb, :live_view - alias Guildhall.OpsDb.{Repo, GovernedArtifact, DeploymentState, VerificationResult} + alias Guildhall.OpsDb.{Repo, GovernedArtifact, DeploymentState, VerificationResult, Guilds} import Ecto.Query @impl true @@ -35,6 +35,9 @@ defmodule GuildhallWeb.DashboardLive do |> assign(:healthy_count, count_by_drift("match")) |> assign(:drifted_count, count_by_drift("drift")) |> assign(:recent_verifications, recent_verifications(5)) + |> assign(:guild_total, Guilds.guild_count()) + |> assign(:guild_pending, Guilds.guild_count("pending_approval")) + |> assign(:guild_active, Guilds.guild_count("active")) end defp count_by_drift(status) do @@ -77,7 +80,23 @@ defmodule GuildhallWeb.DashboardLive do +
+
+
Guilds
+
{@guild_total}
+
+
+
Pending
+
{@guild_pending}
+
+
+
Active
+
{@guild_active}
+
+
+ diff --git a/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/index.ex b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/index.ex new file mode 100644 index 0000000..42d00b3 --- /dev/null +++ b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/index.ex @@ -0,0 +1,147 @@ +defmodule GuildhallWeb.GuildLive.Index do + use GuildhallWeb, :live_view + + alias Guildhall.OpsDb.Guilds + alias Guildhall.Orchestrator.CeremonyClient + + @hub_operator_did "did:web:guildhouse.dev:user:tking" + + @impl true + def mount(_params, _session, socket) do + if connected?(socket) do + Phoenix.PubSub.subscribe(Guildhall.PubSub, "guild:*") + end + + {:ok, + socket + |> assign(:page_title, "Guilds") + |> assign(:guilds, Guilds.list_guilds()) + |> assign(:is_hub_operator, socket.assigns.current_user["did"] == @hub_operator_did)} + end + + @impl true + def handle_event("approve_guild", %{"id" => id}, socket) do + guild = Guilds.get_guild!(id) + + case CeremonyClient.approve_ceremony( + guild.registration_ceremony_id, + socket.assigns.current_user["did"], + "hub_operator" + ) do + {:ok, %{status: "approved"}} -> + {:ok, _} = Guilds.update_guild(guild, %{status: "approved"}) + + {:noreply, + socket + |> put_flash(:info, "#{guild.name} approved.") + |> assign(:guilds, Guilds.list_guilds())} + + {:ok, _} -> + {:noreply, + socket + |> put_flash(:info, "Approval recorded for #{guild.name}.") + |> assign(:guilds, Guilds.list_guilds())} + + {:error, reason} -> + {:noreply, put_flash(socket, :error, "Approval failed: #{inspect(reason)}")} + end + end + + @impl true + def handle_event("deny_guild", %{"id" => id}, socket) do + guild = Guilds.get_guild!(id) + + case CeremonyClient.deny_ceremony( + guild.registration_ceremony_id, + socket.assigns.current_user["did"], + "hub_operator", + "Denied by hub operator" + ) do + {:ok, _} -> + {:ok, _} = Guilds.update_guild(guild, %{status: "denied"}) + + {:noreply, + socket + |> put_flash(:info, "#{guild.name} denied.") + |> assign(:guilds, Guilds.list_guilds())} + + {:error, reason} -> + {:noreply, put_flash(socket, :error, "Denial failed: #{inspect(reason)}")} + end + end + + @impl true + def handle_info({:guild_updated, _}, socket) do + {:noreply, assign(socket, :guilds, Guilds.list_guilds())} + end + + @impl true + def handle_info(_msg, socket), do: {:noreply, socket} + + defp status_class("pending_approval"), do: "text-amber-600" + defp status_class("approved"), do: "text-emerald-600" + defp status_class("active"), do: "text-emerald-600" + defp status_class("denied"), do: "text-red-600" + defp status_class("suspended"), do: "text-zinc-500" + defp status_class(_), do: "text-zinc-500" + + defp type_label("msp"), do: "MSP" + defp type_label("isv"), do: "ISV" + defp type_label("nsp"), do: "NSP" + defp type_label(other), do: String.upcase(other) + + @impl true + def render(assigns) do + ~H""" +
+
+
+ <.link navigate={~p"/"} class="text-sm text-blue-600 underline">← Dashboard +

Guilds

+

{length(@guilds)} registered.

+
+ <.link navigate={~p"/guilds/register"} class="rounded bg-blue-600 px-4 py-2 text-sm text-white"> + Register Guild + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeGuild IDStatusRegistrantActions
+ <.link navigate={~p"/guilds/#{g.slug}"} class="text-blue-600 underline">{g.name} + {type_label(g.guild_type)}0x{Integer.to_string(g.guild_id, 16) |> String.pad_leading(3, "0")}{g.status}{g.registrant_did} +
+ + +
+
No guilds registered yet.
+
+ """ + end +end diff --git a/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/join.ex b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/join.ex new file mode 100644 index 0000000..6972426 --- /dev/null +++ b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/join.ex @@ -0,0 +1,100 @@ +defmodule GuildhallWeb.GuildLive.Join do + use GuildhallWeb, :live_view + + alias Guildhall.OpsDb.{Guilds, GuildMemberships} + alias Guildhall.Orchestrator.CeremonyClient + + @impl true + def mount(%{"slug" => slug}, _session, socket) do + guild = Guilds.get_guild_by_slug(slug) || raise Ecto.NoResultsError, queryable: Guildhall.OpsDb.Guild + user = socket.assigns.current_user + existing = GuildMemberships.find_membership(guild.id, user["did"]) + + if existing do + {:ok, + socket + |> put_flash(:info, "You already have a membership in #{guild.name}.") + |> push_navigate(to: ~p"/guilds/#{slug}")} + else + {:ok, + socket + |> assign(:page_title, "Join #{guild.name}") + |> assign(:guild, guild) + |> assign(:submitted, false)} + end + end + + @impl true + def handle_event("request_membership", _params, socket) do + guild = socket.assigns.guild + user = socket.assigns.current_user + + case GuildMemberships.request_membership(%{ + guild_id: guild.id, + user_did: user["did"], + user_email: user["email"], + display_name: user["name"], + keycloak_sub: user["sub"], + role: "apprentice", + status: "pending" + }) do + {:ok, membership} -> + masters = GuildMemberships.masters_for_guild(guild.id) + approver_did = if masters != [], do: hd(masters).user_did, else: guild.registrant_did + + case CeremonyClient.create_guild_registration_ceremony( + "membership:#{guild.slug}:#{user["preferred_username"]}", + user["did"], + approver_did + ) do + {:ok, response} -> + GuildMemberships.get_membership!(membership.id) + |> Guildhall.OpsDb.GuildMembership.changeset(%{ + membership_ceremony_id: response.ceremony_id + }) + |> Guildhall.OpsDb.Repo.update() + + {:error, _} -> + :ok + end + + {:noreply, + socket + |> assign(:submitted, true) + |> put_flash(:info, "Membership request submitted.")} + + {:error, _changeset} -> + {:noreply, put_flash(socket, :error, "Failed to submit membership request.")} + end + end + + @impl true + def render(assigns) do + ~H""" +
+
+ <.link navigate={~p"/guilds/#{@guild.slug}"} class="text-sm text-blue-600 underline">← {@guild.name} +

Join {@guild.name}

+
+ +
+

{@guild.description || "No description."}

+

Type: {String.upcase(@guild.guild_type)}

+
+ +
+ +
+ +
+ Your membership request has been submitted. A guild master will review it. +
+
+ """ + end +end diff --git a/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/members.ex b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/members.ex new file mode 100644 index 0000000..6fc43f4 --- /dev/null +++ b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/members.ex @@ -0,0 +1,142 @@ +defmodule GuildhallWeb.GuildLive.Members do + use GuildhallWeb, :live_view + + alias Guildhall.OpsDb.{Guilds, GuildMemberships} + alias Guildhall.Orchestrator.CeremonyClient + + @impl true + def mount(%{"slug" => slug}, _session, socket) do + guild = Guilds.get_guild_by_slug(slug) || raise Ecto.NoResultsError, queryable: Guildhall.OpsDb.Guild + user = socket.assigns.current_user + user_membership = GuildMemberships.find_membership(guild.id, user["did"]) + + is_master = user_membership && user_membership.role == "master" && user_membership.status == "active" + + unless is_master do + {:ok, + socket + |> put_flash(:error, "Only guild masters can manage members.") + |> push_navigate(to: ~p"/guilds/#{slug}")} + else + {:ok, + socket + |> assign(:page_title, "#{guild.name} — Members") + |> assign(:guild, guild) + |> assign(:memberships, GuildMemberships.list_memberships(guild.id))} + end + end + + @impl true + def handle_event("approve_member", %{"id" => id}, socket) do + membership = GuildMemberships.get_membership!(id) + + if membership.membership_ceremony_id do + CeremonyClient.approve_ceremony( + membership.membership_ceremony_id, + socket.assigns.current_user["did"], + "guild_master" + ) + end + + {:ok, _} = GuildMemberships.approve_membership(membership, socket.assigns.current_user["did"]) + + {:noreply, + socket + |> put_flash(:info, "Member approved.") + |> assign(:memberships, GuildMemberships.list_memberships(socket.assigns.guild.id))} + end + + @impl true + def handle_event("deny_member", %{"id" => id}, socket) do + membership = GuildMemberships.get_membership!(id) + + if membership.membership_ceremony_id do + CeremonyClient.deny_ceremony( + membership.membership_ceremony_id, + socket.assigns.current_user["did"], + "guild_master", + "Denied by guild master" + ) + end + + {:ok, _} = GuildMemberships.deny_membership(membership, socket.assigns.current_user["did"]) + + {:noreply, + socket + |> put_flash(:info, "Member denied.") + |> assign(:memberships, GuildMemberships.list_memberships(socket.assigns.guild.id))} + end + + @impl true + def handle_event("update_role", %{"id" => id, "role" => role}, socket) do + membership = GuildMemberships.get_membership!(id) + {:ok, _} = GuildMemberships.update_role(membership, role) + + {:noreply, + socket + |> put_flash(:info, "Role updated.") + |> assign(:memberships, GuildMemberships.list_memberships(socket.assigns.guild.id))} + end + + defp status_class("pending"), do: "text-amber-600" + defp status_class("active"), do: "text-emerald-600" + defp status_class("denied"), do: "text-red-600" + defp status_class("suspended"), do: "text-zinc-500" + defp status_class(_), do: "text-zinc-500" + + @impl true + def render(assigns) do + ~H""" +
+
+ <.link navigate={~p"/guilds/#{@guild.slug}"} class="text-sm text-blue-600 underline">← {@guild.name} +

Members

+

{length(@memberships)} member(s).

+
+ + + + + + + + + + + + + + + + + + + + + + + +
NameEmailRoleStatusActions
{m.display_name || "—"}{m.user_email}{m.role}{m.status} +
+ + +
+
+
+ + +
+
+
No members yet.
+
+ """ + end +end diff --git a/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/realization.ex b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/realization.ex new file mode 100644 index 0000000..0dda3fb --- /dev/null +++ b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/realization.ex @@ -0,0 +1,102 @@ +defmodule GuildhallWeb.GuildLive.Realization do + use GuildhallWeb, :live_view + + alias Guildhall.OpsDb.{Guilds, GuildSchematics} + + @impl true + def mount(%{"slug" => slug}, _session, socket) do + guild = Guilds.get_guild_by_slug(slug) || raise Ecto.NoResultsError, queryable: Guildhall.OpsDb.Guild + schematic = GuildSchematics.get_for_guild(guild.id) + + if connected?(socket) do + Phoenix.PubSub.subscribe(Guildhall.PubSub, "realization:#{slug}") + end + + snapshot = if schematic, do: schematic.realization_snapshot, else: %{} + + {:ok, + socket + |> assign(:page_title, "#{guild.name} — Realization") + |> assign(:guild, guild) + |> assign(:schematic, schematic) + |> assign(:snapshot, snapshot)} + end + + @impl true + def handle_info({:realization_update, snapshot}, socket) do + {:noreply, assign(socket, :snapshot, snapshot)} + end + + @impl true + def handle_info(_msg, socket), do: {:noreply, socket} + + @reconciler_sections ~w(trust_domain identity_authority members infrastructure ceremonies federation_peers attestation) + + defp section_status(snapshot, name) do + sections = Map.get(snapshot, "sections", Map.get(snapshot, :sections, [])) + + case Enum.find(sections, fn s -> (s["name"] || s[:name]) == name end) do + nil -> %{status: "pending", message: "Not started"} + s -> %{status: s["status"] || s[:status], message: s["message"] || s[:message] || ""} + end + end + + defp status_icon("succeeded"), do: "text-emerald-600" + defp status_icon("in_progress"), do: "text-amber-600" + defp status_icon("failed"), do: "text-red-600" + defp status_icon("pending"), do: "text-zinc-400" + defp status_icon(_), do: "text-zinc-400" + + defp status_symbol("succeeded"), do: "✓" + defp status_symbol("in_progress"), do: "⟳" + defp status_symbol("failed"), do: "✗" + defp status_symbol(_), do: "○" + + defp overall_class(snapshot) do + overall = Map.get(snapshot, "overall_status", Map.get(snapshot, :overall_status, "pending")) + + cond do + String.contains?(to_string(overall), "REALIZED") -> "bg-emerald-100 text-emerald-700" + String.contains?(to_string(overall), "PUBLISHED") -> "bg-amber-100 text-amber-700" + true -> "bg-zinc-100 text-zinc-700" + end + end + + @impl true + def render(assigns) do + assigns = assign(assigns, :sections, @reconciler_sections) + + ~H""" +
+
+ <.link navigate={~p"/guilds/#{@guild.slug}"} class="text-sm text-blue-600 underline">← {@guild.name} +

Realization Dashboard

+
+ {Map.get(@snapshot, "overall_status", Map.get(@snapshot, :overall_status, "pending"))} +
+
+ +
+ No schematic deployed for this guild yet. +
+ +
+
+ Template: {@schematic.template_name} · + Schematic: {@schematic.schematic_name} v{@schematic.schematic_version} +
+ +
+ <% s = section_status(@snapshot, name) %> + {status_symbol(s.status)} +
+
{String.replace(name, "_", " ") |> String.capitalize()}
+
{s.message}
+
+ {s.status} +
+
+
+ """ + end +end diff --git a/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/register.ex b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/register.ex new file mode 100644 index 0000000..4e6ed9a --- /dev/null +++ b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/register.ex @@ -0,0 +1,148 @@ +defmodule GuildhallWeb.GuildLive.Register do + use GuildhallWeb, :live_view + + alias Guildhall.OpsDb.{Guild, Guilds} + alias Guildhall.Orchestrator.CeremonyClient + + @impl true + def mount(_params, _session, socket) do + changeset = Guild.changeset(%Guild{}, %{}) + + {:ok, + socket + |> assign(:page_title, "Register Guild") + |> assign(:form, to_form(changeset)) + |> assign(:submitting, false)} + end + + @impl true + def handle_event("validate", %{"guild" => params}, socket) do + changeset = + %Guild{} + |> Guild.changeset(params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :form, to_form(changeset))} + end + + @impl true + def handle_event("register", %{"guild" => params}, socket) do + user = socket.assigns.current_user + guild_id = Guilds.next_guild_id() + slug = slugify(params["name"] || "") + + attrs = + params + |> Map.put("guild_id", guild_id) + |> Map.put("slug", slug) + |> Map.put("registrant_did", user["did"]) + |> Map.put("contact_did", user["did"]) + |> Map.put("trust_domain", "#{slug}.guildhouse.dev") + |> Map.put("status", "pending_approval") + + case Guilds.create_guild(attrs) do + {:ok, guild} -> + ceremony_result = + CeremonyClient.create_guild_registration_ceremony( + guild.name, + user["did"], + "did:web:guildhouse.dev:user:tking" + ) + + case ceremony_result do + {:ok, response} -> + Guilds.update_guild(guild, %{registration_ceremony_id: response.ceremony_id}) + + {:error, _reason} -> + :ok + end + + {:noreply, + socket + |> put_flash(:info, "Guild registration submitted for approval.") + |> push_navigate(to: ~p"/guilds/#{guild.slug}")} + + {:error, changeset} -> + {:noreply, assign(socket, :form, to_form(changeset))} + end + end + + @impl true + def render(assigns) do + ~H""" +
+
+ <.link navigate={~p"/guilds"} class="text-sm text-blue-600 underline">← Guilds +

Register Guild

+

Submit a new guild for hub operator approval.

+
+ + <.form for={@form} phx-change="validate" phx-submit="register" class="space-y-4"> +
+ + +

+ {error_to_string(@form[:name].errors)} +

+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ """ + end + + defp error_to_string(errors) do + Enum.map_join(errors, ", ", fn {msg, _opts} -> msg end) + end + + defp slugify(name) do + name + |> String.downcase() + |> String.replace(~r/[^a-z0-9\s-]/, "") + |> String.replace(~r/[\s]+/, "-") + |> String.trim("-") + end +end diff --git a/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/schematic.ex b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/schematic.ex new file mode 100644 index 0000000..2ccea71 --- /dev/null +++ b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/schematic.ex @@ -0,0 +1,149 @@ +defmodule GuildhallWeb.GuildLive.Schematic do + use GuildhallWeb, :live_view + + alias Guildhall.OpsDb.{Guilds, GuildSchematics} + alias Guildhall.Orchestrator.{SchematicClient, SchematicTemplate, RealizationPoller} + + @impl true + def mount(%{"slug" => slug}, _session, socket) do + guild = Guilds.get_guild_by_slug(slug) || raise Ecto.NoResultsError, queryable: Guildhall.OpsDb.Guild + + unless guild.status in ["approved", "active"] do + {:ok, + socket + |> put_flash(:error, "Guild must be approved before deploying a schematic.") + |> push_navigate(to: ~p"/guilds/#{slug}")} + else + existing = GuildSchematics.get_for_guild(guild.id) + + if existing do + {:ok, + socket + |> put_flash(:info, "Schematic already deployed.") + |> push_navigate(to: ~p"/guilds/#{slug}/realization")} + else + template_result = SchematicTemplate.load_template(guild.guild_type) + + {:ok, + socket + |> assign(:page_title, "#{guild.name} — Deploy Schematic") + |> assign(:guild, guild) + |> assign(:template, elem_ok(template_result)) + |> assign(:template_error, elem_error(template_result)) + |> assign(:deploying, false) + |> assign(:deploy_error, nil)} + end + end + end + + @impl true + def handle_event("deploy_schematic", _params, socket) do + guild = socket.assigns.guild + template = socket.assigns.template + socket = assign(socket, :deploying, true) + + params = %{ + "guild_slug" => guild.slug, + "guild_name" => guild.name, + "trust_domain" => guild.trust_domain || "#{guild.slug}.guildhouse.dev", + "registrant_did" => guild.registrant_did + } + + rendered = SchematicTemplate.render_template(template, params) + operations = SchematicTemplate.to_fork_operations(rendered) + {source_name, source_version} = SchematicTemplate.source_schematic(template) + schematic_name = "#{guild.slug}-#{guild.guild_type}-founding" + schematic_version = "1.0.0" + + with {:ok, fork_resp} <- + SchematicClient.fork_schematic(source_name, source_version, schematic_name, schematic_version, operations), + {:ok, binding_resp} <- + SchematicClient.create_deployment_binding(schematic_name, schematic_version, "founding", "production"), + {:ok, realize_resp} <- + SchematicClient.realize_ffc_schematic(schematic_name, schematic_version) do + {:ok, gs} = + GuildSchematics.create(%{ + guild_id: guild.id, + template_name: "#{guild.guild_type}-founding", + schematic_name: schematic_name, + schematic_version: schematic_version, + tree_hash: Map.get(fork_resp, :tree_hash, ""), + binding_id: Map.get(binding_resp, :binding_id, ""), + realization_id: Map.get(realize_resp, :realization_id, ""), + status: "realizing" + }) + + if gs.realization_id && gs.realization_id != "" do + RealizationPoller.watch(gs.realization_id, guild.slug) + end + + {:ok, _} = Guilds.update_guild(guild, %{status: "active"}) + + {:noreply, + socket + |> put_flash(:info, "Schematic deployed. Realization in progress.") + |> push_navigate(to: ~p"/guilds/#{guild.slug}/realization")} + else + {:error, reason} -> + {:noreply, + socket + |> assign(:deploying, false) + |> assign(:deploy_error, inspect(reason))} + end + end + + defp elem_ok({:ok, val}), do: val + defp elem_ok(_), do: nil + + defp elem_error({:error, reason}), do: inspect(reason) + defp elem_error(_), do: nil + + @impl true + def render(assigns) do + ~H""" +
+
+ <.link navigate={~p"/guilds/#{@guild.slug}"} class="text-sm text-blue-600 underline">← {@guild.name} +

Deploy Founding Schematic

+
+ +
+ Failed to load template: {@template_error} +
+ +
+
+

+ Template: {@guild.guild_type}-founding +

+
+
{inspect(@template, pretty: true, limit: :infinity)}
+
+
+ +
+

This will:

+
    +
  1. Fork the founding schematic template for {@guild.name}
  2. +
  3. Create a deployment binding (production)
  4. +
  5. Trigger realization across all reconciler sections
  6. +
  7. Transition guild to active status
  8. +
+
+ +
+ Deploy failed: {@deploy_error} +
+ + +
+
+ """ + end +end diff --git a/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/show.ex b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/show.ex new file mode 100644 index 0000000..8c4a113 --- /dev/null +++ b/apps/guildhall_web/lib/guildhall_web_web/live/guild_live/show.ex @@ -0,0 +1,227 @@ +defmodule GuildhallWeb.GuildLive.Show do + use GuildhallWeb, :live_view + + alias Guildhall.OpsDb.{Guilds, GuildSchematics, GuildMemberships} + alias Guildhall.Orchestrator.CeremonyClient + + @hub_operator_did "did:web:guildhouse.dev:user:tking" + @poll_interval_ms 5_000 + + @impl true + def mount(%{"slug" => slug}, _session, socket) do + guild = Guilds.get_guild_by_slug(slug) || raise Ecto.NoResultsError, queryable: Guildhall.OpsDb.Guild + + if connected?(socket) do + Phoenix.PubSub.subscribe(Guildhall.PubSub, "guild:#{slug}") + + if guild.status == "pending_approval" && guild.registration_ceremony_id do + Process.send_after(self(), :poll_ceremony, @poll_interval_ms) + end + end + + {:ok, + socket + |> assign(:page_title, guild.name) + |> assign(:guild, guild) + |> assign(:ceremony_status, nil) + |> assign(:is_hub_operator, socket.assigns.current_user["did"] == @hub_operator_did) + |> assign(:schematic, GuildSchematics.get_for_guild(guild.id)) + |> assign(:member_count, length(GuildMemberships.active_members(guild.id)))} + end + + @impl true + def handle_info(:poll_ceremony, socket) do + guild = socket.assigns.guild + + case CeremonyClient.get_ceremony(guild.registration_ceremony_id) do + {:ok, response} -> + socket = assign(socket, :ceremony_status, response.status) + + socket = + case response.status do + "approved" -> + {:ok, %{guild: updated}} = Guilds.approve_guild(guild) + assign(socket, :guild, updated) + + "denied" -> + {:ok, updated} = Guilds.update_guild(guild, %{status: "denied"}) + assign(socket, :guild, updated) + + _ -> + Process.send_after(self(), :poll_ceremony, @poll_interval_ms) + socket + end + + {:noreply, socket} + + {:error, _} -> + Process.send_after(self(), :poll_ceremony, @poll_interval_ms) + {:noreply, socket} + end + end + + @impl true + def handle_info({:guild_updated, _}, socket) do + guild = Guilds.get_guild_by_slug(socket.assigns.guild.slug) + {:noreply, assign(socket, :guild, guild)} + end + + @impl true + def handle_info(_msg, socket), do: {:noreply, socket} + + @impl true + def handle_event("approve_guild", _params, socket) do + guild = socket.assigns.guild + + case CeremonyClient.approve_ceremony( + guild.registration_ceremony_id, + socket.assigns.current_user["did"], + "hub_operator" + ) do + {:ok, %{status: "approved"}} -> + {:ok, %{guild: updated}} = Guilds.approve_guild(guild) + + {:noreply, + socket + |> assign(:guild, updated) + |> assign(:ceremony_status, "approved") + |> put_flash(:info, "#{guild.name} approved.")} + + {:ok, _} -> + {:noreply, put_flash(socket, :info, "Approval recorded.")} + + {:error, reason} -> + {:noreply, put_flash(socket, :error, "Approval failed: #{inspect(reason)}")} + end + end + + @impl true + def handle_event("deny_guild", _params, socket) do + guild = socket.assigns.guild + + case CeremonyClient.deny_ceremony( + guild.registration_ceremony_id, + socket.assigns.current_user["did"], + "hub_operator", + "Denied by hub operator" + ) do + {:ok, _} -> + {:ok, updated} = Guilds.update_guild(guild, %{status: "denied"}) + + {:noreply, + socket + |> assign(:guild, updated) + |> assign(:ceremony_status, "denied") + |> put_flash(:info, "#{guild.name} denied.")} + + {:error, reason} -> + {:noreply, put_flash(socket, :error, "Denial failed: #{inspect(reason)}")} + end + end + + defp status_class("pending_approval"), do: "bg-amber-100 text-amber-700" + defp status_class("approved"), do: "bg-emerald-100 text-emerald-700" + defp status_class("active"), do: "bg-emerald-100 text-emerald-700" + defp status_class("denied"), do: "bg-red-100 text-red-700" + defp status_class("suspended"), do: "bg-zinc-100 text-zinc-700" + defp status_class(_), do: "bg-zinc-100 text-zinc-700" + + defp schematic_status_class("realized"), do: "text-emerald-600" + defp schematic_status_class("realizing"), do: "text-amber-600" + defp schematic_status_class("failed"), do: "text-red-600" + defp schematic_status_class(_), do: "text-zinc-500" + + defp type_label("msp"), do: "Managed Service Provider" + defp type_label("isv"), do: "Independent Software Vendor" + defp type_label("nsp"), do: "Network Service Provider" + defp type_label(other), do: other + + @impl true + def render(assigns) do + ~H""" +
+
+ <.link navigate={~p"/guilds"} class="text-sm text-blue-600 underline">← Guilds +

{@guild.name}

+ + {@guild.status} + +
+ +
+
+
Guild Type
+
{type_label(@guild.guild_type)}
+ +
Guild ID
+
0x{Integer.to_string(@guild.guild_id, 16) |> String.pad_leading(3, "0")}
+ +
Trust Domain
+
{@guild.trust_domain || "—"}
+ +
Registrant
+
{@guild.registrant_did}
+ +
Contact
+
{@guild.contact_did}
+
+ +
+
Description
+

{@guild.description}

+
+
+ +
+

Awaiting Approval

+

+ Ceremony: {@guild.registration_ceremony_id} +

+

+ Status: {@ceremony_status} +

+ +
+ + +
+
+ +
+

This guild registration was denied.

+
+ +
+
+ Guild is active and operational. {@member_count} active member(s). +
+ +
+ <.link :if={@schematic == nil} navigate={~p"/guilds/#{@guild.slug}/schematic"} class="rounded bg-blue-600 px-3 py-1.5 text-white"> + Deploy Schematic + + <.link :if={@schematic} navigate={~p"/guilds/#{@guild.slug}/realization"} class="rounded bg-zinc-700 px-3 py-1.5 text-white"> + Realization Dashboard + + <.link navigate={~p"/guilds/#{@guild.slug}/join"} class="rounded border border-blue-600 px-3 py-1.5 text-blue-600"> + Join Guild + + <.link navigate={~p"/guilds/#{@guild.slug}/members"} class="rounded border border-zinc-300 px-3 py-1.5 text-zinc-700"> + Manage Members + +
+ +
+
Schematic
+ {@schematic.schematic_name} v{@schematic.schematic_version} + {@schematic.status} +
+
+
+ """ + end +end diff --git a/apps/guildhall_web/lib/guildhall_web_web/plugs/auth.ex b/apps/guildhall_web/lib/guildhall_web_web/plugs/auth.ex new file mode 100644 index 0000000..4c06a75 --- /dev/null +++ b/apps/guildhall_web/lib/guildhall_web_web/plugs/auth.ex @@ -0,0 +1,24 @@ +defmodule GuildhallWeb.Plugs.Auth do + @moduledoc false + import Plug.Conn + import Phoenix.Controller + + def init(opts), do: opts + + def call(conn, _opts) do + case get_session(conn, :current_user) do + nil -> + conn + |> put_flash(:error, "Please sign in to continue.") + |> redirect(to: "/auth/login") + |> halt() + + user -> + assign(conn, :current_user, user) + end + end + + def fetch_current_user(conn, _opts) do + assign(conn, :current_user, get_session(conn, :current_user)) + end +end diff --git a/apps/guildhall_web/lib/guildhall_web_web/router.ex b/apps/guildhall_web/lib/guildhall_web_web/router.ex index 1ae5060..775feb9 100644 --- a/apps/guildhall_web/lib/guildhall_web_web/router.ex +++ b/apps/guildhall_web/lib/guildhall_web_web/router.ex @@ -1,6 +1,8 @@ defmodule GuildhallWeb.Router do use GuildhallWeb, :router + import GuildhallWeb.Plugs.Auth, only: [fetch_current_user: 2] + pipeline :browser do plug :accepts, ["html"] plug :fetch_session @@ -8,24 +10,41 @@ defmodule GuildhallWeb.Router do plug :put_root_layout, html: {GuildhallWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers + plug :fetch_current_user end pipeline :api do plug :accepts, ["json"] end + # Public auth routes (no auth required) + scope "/auth", GuildhallWeb do + pipe_through :browser + + get "/login", AuthController, :login + get "/callback", AuthController, :callback + get "/logout", AuthController, :logout + end + + # Authenticated LiveView routes scope "/", GuildhallWeb do pipe_through :browser - live "/", DashboardLive, :index - live "/ceremonies", CeremonyLive.Index, :index - live "/artifacts", ArtifactLive.Index, :index + live_session :authenticated, on_mount: {GuildhallWeb.AuthHooks, :require_auth} do + live "/", DashboardLive, :index + live "/ceremonies", CeremonyLive.Index, :index + live "/artifacts", ArtifactLive.Index, :index + live "/guilds", GuildLive.Index, :index + live "/guilds/register", GuildLive.Register, :new + live "/guilds/:slug", GuildLive.Show, :show + live "/guilds/:slug/schematic", GuildLive.Schematic, :schematic + live "/guilds/:slug/realization", GuildLive.Realization, :realization + live "/guilds/:slug/join", GuildLive.Join, :join + live "/guilds/:slug/members", GuildLive.Members, :members + end end # Health check endpoint for Kubernetes probes + LB targets. - # GET /health — returns 200 when Phoenix is up AND the Ecto pool - # can query the DB; 503 otherwise. Unauthenticated (the whole point - # is that it's reachable without credentials). scope "/health", GuildhallWeb do pipe_through :api get "/", HealthController, :check diff --git a/apps/guildhall_web/mix.exs b/apps/guildhall_web/mix.exs index 20ebd7c..6a640cd 100644 --- a/apps/guildhall_web/mix.exs +++ b/apps/guildhall_web/mix.exs @@ -65,7 +65,9 @@ defmodule GuildhallWeb.MixProject do {:gettext, "~> 1.0"}, {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.2.0"}, - {:bandit, "~> 1.5"} + {:bandit, "~> 1.5"}, + {:oidcc, "~> 3.2"}, + {:req, "~> 0.5"} ] end diff --git a/config/dev.exs b/config/dev.exs index 604a8c1..aa5ac20 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -44,3 +44,14 @@ config :phoenix_live_view, debug_heex_annotations: true, debug_attributes: true, enable_expensive_runtime_checks: true + +config :guildhall_web, :oidc, + issuer: "https://auth.guildhouse.dev/realms/guildhouse", + client_id: "guildhall-web", + client_secret: System.get_env("OIDC_CLIENT_SECRET"), + redirect_uri: "http://localhost:4000/auth/callback" + +config :guildhall_orchestrator, + ceremony_service_url: "localhost:50053", + schematic_service_url: "localhost:9091", + ffc_schematic_service_url: "localhost:9091" diff --git a/config/runtime.exs b/config/runtime.exs index 9fe11fe..6e6110c 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -43,14 +43,20 @@ if config_env() == :prod do http: [ip: {0, 0, 0, 0, 0, 0, 0, 0}], secret_key_base: secret_key_base - # K8s cluster connection (future — orchestrator will use this) - # config :guildhall_orchestrator, - # kubeconfig: System.get_env("KUBECONFIG") || "~/.kube/config", - # context: System.get_env("K8S_CONTEXT") + config :guildhall_web, :oidc, + issuer: + System.get_env("OIDC_ISSUER") || "https://auth.guildhouse.dev/realms/guildhouse", + client_id: System.get_env("OIDC_CLIENT_ID") || "guildhall-web", + client_secret: System.get_env("OIDC_CLIENT_SECRET"), + redirect_uri: + System.get_env("OIDC_REDIRECT_URI") || + "https://guildhall.guildhouse.dev/auth/callback" - # Keycloak OIDC (future — auth) - # config :guildhall_web, :oidc, - # issuer: System.get_env("OIDC_ISSUER") || "https://auth.guildhouse.dev/realms/guildhouse", - # client_id: System.get_env("OIDC_CLIENT_ID"), - # client_secret: System.get_env("OIDC_CLIENT_SECRET") + config :guildhall_orchestrator, + ceremony_service_url: + System.get_env("CEREMONY_SERVICE_URL") || "localhost:50053", + schematic_service_url: + System.get_env("SCHEMATIC_SERVICE_URL") || "localhost:9091", + ffc_schematic_service_url: + System.get_env("FFC_SCHEMATIC_SERVICE_URL") || "localhost:9091" end diff --git a/k8s/50-guildhall-secrets-template.yaml b/k8s/50-guildhall-secrets-template.yaml index 611ad92..0f81a56 100644 --- a/k8s/50-guildhall-secrets-template.yaml +++ b/k8s/50-guildhall-secrets-template.yaml @@ -37,9 +37,12 @@ # # SECRET_KEY_BASE="$(cd /home/tking/projects/substrate-project/guildhall && mix phx.gen.secret)" # +# OIDC_CLIENT_SECRET="" +# # kubectl create secret generic guildhall-app-secrets \ # --from-literal=SECRET_KEY_BASE="$SECRET_KEY_BASE" \ # --from-literal=DATABASE_URL="ecto://guildhall:$DB_PASSWORD@guildhall-postgres:5432/guildhall" \ +# --from-literal=OIDC_CLIENT_SECRET="$OIDC_CLIENT_SECRET" \ # --namespace=guildhall # # Note: `ecto://` scheme, not `postgres://` — `config/runtime.exs` @@ -55,5 +58,20 @@ # namespace: guildhall # type: Opaque # data: -# SECRET_KEY_BASE: "> -# DATABASE_URL: @guildhall-postgres:5432/guildhall"> +# SECRET_KEY_BASE: "> +# DATABASE_URL: @guildhall-postgres:5432/guildhall"> +# OIDC_CLIENT_SECRET: "> +# +# ---------- ceremony-service-secrets ---------- +# Consumed by the ceremony-service Deployment. +# +# kubectl create secret generic ceremony-service-secrets \ +# --from-literal=DATABASE_URL="postgres://ceremony:$CEREMONY_DB_PW@guildhall-postgres:5432/ceremony" \ +# --namespace=guildhall +# +# ---------- schematic-server-secrets ---------- +# Consumed by the ffc-schematic-server Deployment. +# +# kubectl create secret generic schematic-server-secrets \ +# --from-literal=DATABASE_URL="postgres://schematic:$SCHEMATIC_DB_PW@guildhall-postgres:5432/schematic" \ +# --namespace=guildhall diff --git a/k8s/70-guildhall-deployment.yaml b/k8s/70-guildhall-deployment.yaml index 0871608..385d1c6 100644 --- a/k8s/70-guildhall-deployment.yaml +++ b/k8s/70-guildhall-deployment.yaml @@ -54,6 +54,25 @@ spec: secretKeyRef: name: guildhall-app-secrets key: SECRET_KEY_BASE + # OIDC (Keycloak) + - name: OIDC_ISSUER + value: "https://auth.guildhouse.dev/realms/guildhouse" + - name: OIDC_CLIENT_ID + value: guildhall-web + - name: OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: guildhall-app-secrets + key: OIDC_CLIENT_SECRET + - name: OIDC_REDIRECT_URI + value: "https://guildhall.guildhouse.dev/auth/callback" + # gRPC service URLs (in-cluster ClusterIP DNS) + - name: CEREMONY_SERVICE_URL + value: "ceremony-service.guildhall.svc.cluster.local:50053" + - name: SCHEMATIC_SERVICE_URL + value: "ffc-schematic-server.guildhall.svc.cluster.local:9091" + - name: FFC_SCHEMATIC_SERVICE_URL + value: "ffc-schematic-server.guildhall.svc.cluster.local:9091" # Ecto - name: DATABASE_URL valueFrom: diff --git a/k8s/90-ceremony-service-deployment.yaml b/k8s/90-ceremony-service-deployment.yaml new file mode 100644 index 0000000..0cc227a --- /dev/null +++ b/k8s/90-ceremony-service-deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ceremony-service + namespace: guildhall + labels: + app.kubernetes.io/name: ceremony-service + app.kubernetes.io/part-of: guildhouse + app.kubernetes.io/component: ceremony-engine + app.kubernetes.io/managed-by: manual + app.kubernetes.io/version: v0.1.0 +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: ceremony-service + template: + metadata: + labels: + app: ceremony-service + app.kubernetes.io/name: ceremony-service + app.kubernetes.io/part-of: guildhouse + app.kubernetes.io/component: ceremony-engine + app.kubernetes.io/version: v0.1.0 + spec: + imagePullSecrets: + - name: guildhall-registry + containers: + - name: ceremony-service + image: git.guildhouse.dev/tking/ceremony-service:v0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 50053 + name: grpc + protocol: TCP + env: + - name: RUST_LOG + value: info + - name: LISTEN_ADDR + value: "0.0.0.0:50053" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: ceremony-service-secrets + key: DATABASE_URL + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + readinessProbe: + grpc: + port: 50053 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + grpc: + port: 50053 + initialDelaySeconds: 15 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + terminationGracePeriodSeconds: 15 diff --git a/k8s/91-ceremony-service-service.yaml b/k8s/91-ceremony-service-service.yaml new file mode 100644 index 0000000..e192751 --- /dev/null +++ b/k8s/91-ceremony-service-service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: ceremony-service + namespace: guildhall + labels: + app.kubernetes.io/name: ceremony-service + app.kubernetes.io/part-of: guildhouse + app.kubernetes.io/component: ceremony-engine + app.kubernetes.io/managed-by: manual +spec: + type: ClusterIP + ports: + - port: 50053 + targetPort: grpc + protocol: TCP + name: grpc + selector: + app: ceremony-service diff --git a/k8s/92-schematic-server-deployment.yaml b/k8s/92-schematic-server-deployment.yaml new file mode 100644 index 0000000..6a34d1c --- /dev/null +++ b/k8s/92-schematic-server-deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ffc-schematic-server + namespace: guildhall + labels: + app.kubernetes.io/name: ffc-schematic-server + app.kubernetes.io/part-of: guildhouse + app.kubernetes.io/component: schematic-engine + app.kubernetes.io/managed-by: manual + app.kubernetes.io/version: v0.1.0 +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: ffc-schematic-server + template: + metadata: + labels: + app: ffc-schematic-server + app.kubernetes.io/name: ffc-schematic-server + app.kubernetes.io/part-of: guildhouse + app.kubernetes.io/component: schematic-engine + app.kubernetes.io/version: v0.1.0 + spec: + imagePullSecrets: + - name: guildhall-registry + containers: + - name: ffc-schematic-server + image: git.guildhouse.dev/tking/ffc-schematic-server:v0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 9091 + name: grpc + protocol: TCP + env: + - name: RUST_LOG + value: info + - name: LISTEN_ADDR + value: "0.0.0.0:9091" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: schematic-server-secrets + key: DATABASE_URL + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + readinessProbe: + grpc: + port: 9091 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + grpc: + port: 9091 + initialDelaySeconds: 15 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + terminationGracePeriodSeconds: 15 diff --git a/k8s/93-schematic-server-service.yaml b/k8s/93-schematic-server-service.yaml new file mode 100644 index 0000000..6871942 --- /dev/null +++ b/k8s/93-schematic-server-service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: ffc-schematic-server + namespace: guildhall + labels: + app.kubernetes.io/name: ffc-schematic-server + app.kubernetes.io/part-of: guildhouse + app.kubernetes.io/component: schematic-engine + app.kubernetes.io/managed-by: manual +spec: + type: ClusterIP + ports: + - port: 9091 + targetPort: grpc + protocol: TCP + name: grpc + selector: + app: ffc-schematic-server diff --git a/mix.lock b/mix.lock index 693526f..b4cf7a1 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,8 @@ %{ "bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, + "cowboy": {:hex, :cowboy, "2.15.0", "9cfe86ed7117bf045e10adbedb0170af7be57f2a3637e7be143433d8dd267396", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "179fb65140fb440a17b767ad53b755081506f9596c4db5c49c0396d8c8643668"}, + "cowlib": {:hex, :cowlib, "2.16.1", "318d385d55f657e9a5005838c4e426e13dcd724a691438384b6165a69687e531", [:make, :rebar3], [], "hexpm", "58f1e425a9e04176f1d30e20116f57c4e90ef0e187552e9741c465bdf4044f70"}, "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, @@ -10,13 +12,25 @@ "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, + "finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"}, "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"}, + "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, + "gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"}, "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, + "google_protos": {:hex, :google_protos, "0.4.0", "93e1be2c1a07517ffed761f69047776caf35e4acd385aac4f5ce4fedd07f3660", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "4c54983d78761a3643e2198adf0f5d40a5a8b08162f3fc91c50faa257f3fa19f"}, + "googleapis": {:hex, :googleapis, "0.1.0", "13770f3f75f5b863fb9acf41633c7bc71bad788f3f553b66481a096d083ee20e", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1989a7244fd17d3eb5f3de311a022b656c3736b39740db46506157c4604bd212"}, + "grpc": {:hex, :grpc, "0.11.5", "5dbde9420718b58712779ad98fff1ef50349ca0fa7cc0858ae0f826015068654", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:cowboy, "~> 2.10", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowlib, "~> 2.12", [hex: :cowlib, repo: "hexpm", optional: false]}, {:flow, "~> 1.2", [hex: :flow, repo: "hexpm", optional: false]}, {:googleapis, "~> 0.1.0", [hex: :googleapis, repo: "hexpm", optional: false]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, ">= 0.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.14", [hex: :protobuf, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0a5d8673ef16649bef0903bca01c161acfc148e4d269133b6834b2af1f07f45e"}, + "gun": {:hex, :gun, "2.3.0", "c1eb7be3b5178f6e67edd373f954360de7d7933f2d5a57686affd3b279d76cdf", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "c3bfbbb8f146a6c5ffb2c487f06a3ca4a57e90220b07a1f97eb69a4e7b176035"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"}, "lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "oidcc": {:hex, :oidcc, "3.7.2", "2047949832ca7984d6d9c218cc5f23e8096bf50ebb809124d3a01673ee2bfe12", [:mix, :rebar3], [{:igniter, "~> 0.6.3 or ~> 0.7.0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.1", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "e3f1ed91509fdeb31ec8b9de4ecda0e80cb68b463a9f5b7a9ee1ee40e521e445"}, "phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, @@ -26,12 +40,17 @@ "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"}, + "protobuf": {:hex, :protobuf, "0.16.0", "d1878725105d49162977cf3408ccc3eac4f3532e26e5a9e250f2c624175d10f6", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "f0d0d3edd8768130f24cc2cfc41320637d32c80110e80d13f160fa699102c828"}, + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, + "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, + "telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"}, "tesla": {:hex, :tesla, "1.16.0", "de77d083aea08ebd1982600693ff5d779d68a4bb835d136a0394b08f69714660", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "eb3bdfc0c6c8a23b4e3d86558e812e3577acff1cb4acb6cfe2da1985a1035b89"}, "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, + "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, }