From 4dae8badf1246a0425fe66128533b26c0e0b48ee Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Fri, 21 Jul 2023 09:40:20 -0400 Subject: [PATCH] Support oidc and service account bindings in invites (#1171) --- .dockerignore | 1 + apps/core/lib/core/schema/invite.ex | 8 +++-- apps/core/lib/core/services/accounts.ex | 34 ++++++++++++++++++- .../migrations/20230720191356_invite_oidc.exs | 10 ++++++ apps/core/test/services/accounts_test.exs | 28 +++++++++++++++ apps/graphql/lib/graphql/schema/account.ex | 8 +++-- schema/schema.graphql | 2 ++ www/src/generated/graphql.ts | 2 ++ 8 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 apps/core/priv/repo/migrations/20230720191356_invite_oidc.exs diff --git a/.dockerignore b/.dockerignore index 77b14cc68..da2cbed1b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,4 +13,5 @@ test/ priv/static/ creds/ www/ +ai/ .github/ diff --git a/apps/core/lib/core/schema/invite.ex b/apps/core/lib/core/schema/invite.ex index 25cc1bbf6..8d49a79bb 100644 --- a/apps/core/lib/core/schema/invite.ex +++ b/apps/core/lib/core/schema/invite.ex @@ -1,6 +1,6 @@ defmodule Core.Schema.Invite do use Piazza.Ecto.Schema - alias Core.Schema.{Account, User, InviteGroup} + alias Core.Schema.{Account, User, InviteGroup, OIDCProvider} @email_re ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-\.]+\.[a-zA-Z]{2,}$/ @@ -11,6 +11,8 @@ defmodule Core.Schema.Invite do belongs_to :user, User belongs_to :account, Account + belongs_to :service_account, User + belongs_to :oidc_provider, OIDCProvider has_many :invite_groups, InviteGroup, on_replace: :delete has_many :groups, through: [:invite_groups, :group] @@ -27,7 +29,7 @@ defmodule Core.Schema.Invite do from(i in query, where: i.account_id == ^aid) end - @valid ~w(email account_id user_id admin)a + @valid ~w(email account_id user_id admin service_account_id oidc_provider_id)a def changeset(model, attrs \\ %{}) do model @@ -35,6 +37,8 @@ defmodule Core.Schema.Invite do |> cast_assoc(:invite_groups) |> put_new_change(:secure_id, &gen_external_id/0) |> foreign_key_constraint(:account_id) + |> foreign_key_constraint(:oidc_provider_id) + |> foreign_key_constraint(:service_account_id) |> unique_constraint(:secure_id) |> unique_constraint(:email) |> validate_format(:email, @email_re) diff --git a/apps/core/lib/core/services/accounts.ex b/apps/core/lib/core/services/accounts.ex index cda8a54fb..c35992b19 100644 --- a/apps/core/lib/core/services/accounts.ex +++ b/apps/core/lib/core/services/accounts.ex @@ -12,6 +12,7 @@ defmodule Core.Services.Accounts do Role, IntegrationWebhook, OAuthIntegration, + OIDCProvider, DomainMapping, RoleBinding, ImpersonationPolicyBinding, @@ -431,7 +432,7 @@ defmodule Core.Services.Accounts do @spec realize_invite(map, binary) :: user_resp def realize_invite(attributes, invite_id) do invite = get_invite!(invite_id) - |> Core.Repo.preload([:groups, user: :account]) + |> Core.Repo.preload([:groups, :oidc_provider, :service_account, user: :account]) start_transaction() |> add_operation(:user, fn _ -> @@ -453,6 +454,8 @@ defmodule Core.Services.Accounts do end end) |> add_to_groups(invite) + |> add_bindings(:oidc, invite) + |> add_bindings(:sa, invite) |> add_operation(:invite, fn _ -> Core.Repo.delete(invite) end) |> add_operation(:account, fn %{user: %{id: uid, account: %{root_user_id: uid} = account}} -> @@ -475,6 +478,35 @@ defmodule Core.Services.Accounts do end defp add_to_groups(xact, _), do: xact + defp add_bindings(xact, :oidc, %Invite{oidc_provider: %OIDCProvider{} = provider}) do + add_operation(xact, :oidc, fn %{upsert: %{id: id}} -> + provider = Core.Repo.preload(provider, [:bindings]) + bindings = add_binding(provider.bindings, id) + + OIDCProvider.changeset(provider, %{bindings: bindings}) + |> Core.Repo.update() + end) + end + defp add_bindings(xact, :sa, %Invite{service_account: %User{} = sa}) do + add_operation(xact, :sa, fn %{upsert: %{id: id}} -> + sa = Core.Repo.preload(sa, [impersonation_policy: [:bindings]]) + bindings = add_binding(sa.impersonation_policy.bindings, id) + + User.service_account_changeset(sa, %{impersonation_policy: %{bindings: bindings}}) + |> Core.Repo.update() + end) + end + defp add_bindings(xact, _, _), do: xact + + defp add_binding(bindings, user_id) do + Enum.map([%{user_id: user_id} | bindings], fn + %{user_id: uid} when is_binary(uid) -> %{user_id: uid} + %{group_id: gid} when is_binary(gid) -> %{group_id: gid} + end) + |> MapSet.new() + |> MapSet.to_list() + end + @doc """ Creates a group in the user's account """ diff --git a/apps/core/priv/repo/migrations/20230720191356_invite_oidc.exs b/apps/core/priv/repo/migrations/20230720191356_invite_oidc.exs new file mode 100644 index 000000000..a6c5ffae4 --- /dev/null +++ b/apps/core/priv/repo/migrations/20230720191356_invite_oidc.exs @@ -0,0 +1,10 @@ +defmodule Core.Repo.Migrations.InviteOidc do + use Ecto.Migration + + def change do + alter table(:invites) do + add :oidc_provider_id, references(:oidc_providers, type: :uuid, on_delete: :delete_all) + add :service_account_id, references(:users, type: :uuid, on_delete: :delete_all) + end + end +end diff --git a/apps/core/test/services/accounts_test.exs b/apps/core/test/services/accounts_test.exs index 4eafef90a..0bccab824 100644 --- a/apps/core/test/services/accounts_test.exs +++ b/apps/core/test/services/accounts_test.exs @@ -385,6 +385,34 @@ defmodule Core.Services.AccountsTest do assert_receive {:event, %PubSub.UserCreated{item: ^user}} end + test "it can bind users to service accounts/oidc providers", %{user: user, account: account} do + oidc = insert(:oidc_provider) + sa = insert(:user, service_account: true, account: account) + group = insert(:group, account: account) + insert(:oidc_provider_binding, provider: oidc, group: group) + policy = insert(:impersonation_policy, user: sa) + insert(:impersonation_policy_binding, policy: policy, group: group) + + {:ok, invite} = Accounts.create_invite(%{ + email: "someone@example.com", + oidc_provider_id: oidc.id, + service_account_id: sa.id + }, user) + + {:ok, new_user} = Accounts.realize_invite(%{ + password: "some long password", + name: "Some User" + }, invite.secure_id) + + %{bindings: bindings} = Core.Repo.preload(oidc, [:bindings]) + assert Enum.find_value(bindings, & &1.group_id) == group.id + assert Enum.find_value(bindings, & &1.user_id) == new_user.id + + %{impersonation_policy: %{bindings: bindings}} = Core.Repo.preload(sa, [impersonation_policy: :bindings]) + assert Enum.find_value(bindings, & &1.group_id) == group.id + assert Enum.find_value(bindings, & &1.user_id) == new_user.id + end + test "it can bind users to groups when realizing an invite", %{user: user, account: account} do groups = insert_list(2, :group, account: account) {:ok, invite} = Accounts.create_invite(%{ diff --git a/apps/graphql/lib/graphql/schema/account.ex b/apps/graphql/lib/graphql/schema/account.ex index a46048d8c..57204fdae 100644 --- a/apps/graphql/lib/graphql/schema/account.ex +++ b/apps/graphql/lib/graphql/schema/account.ex @@ -21,9 +21,11 @@ defmodule GraphQl.Schema.Account do end input_object :invite_attributes do - field :email, :string - field :admin, :boolean - field :invite_groups, list_of(:binding_attributes) + field :email, :string + field :admin, :boolean + field :oidc_provider_id, :id + field :service_account_id, :id + field :invite_groups, list_of(:binding_attributes) end input_object :group_attributes do diff --git a/schema/schema.graphql b/schema/schema.graphql index c09d5fb45..9097d3935 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -639,6 +639,8 @@ input FileAttributes { input InviteAttributes { email: String admin: Boolean + oidcProviderId: ID + serviceAccountId: ID inviteGroups: [BindingAttributes] } diff --git a/www/src/generated/graphql.ts b/www/src/generated/graphql.ts index 423dfca66..d62d02060 100644 --- a/www/src/generated/graphql.ts +++ b/www/src/generated/graphql.ts @@ -1387,6 +1387,8 @@ export type InviteAttributes = { admin?: InputMaybe; email?: InputMaybe; inviteGroups?: InputMaybe>>; + oidcProviderId?: InputMaybe; + serviceAccountId?: InputMaybe; }; export type InviteConnection = {