From d211799fe38e546c35da2165be3fd4577c352157 Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Wed, 11 Jan 2023 10:40:46 -0500 Subject: [PATCH] Add ability to autoprovision service account rbac (#853) --- apps/core/lib/core/schema/account.ex | 3 +- apps/core/lib/core/schema/platform_plan.ex | 1 + apps/core/lib/core/services/accounts.ex | 49 +++++++++++++++++++ apps/core/lib/core/services/payments.ex | 2 + ...230109223232_add_role_provisioned_bool.exs | 13 +++++ .../priv/repo/seeds/012_enterprise_plan.exs | 8 +++ apps/core/test/services/accounts_test.exs | 9 ++++ apps/core/test/services/payments_test.exs | 7 +++ apps/graphql/lib/graphql/schema/payments.ex | 1 + 9 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 apps/core/priv/repo/migrations/20230109223232_add_role_provisioned_bool.exs create mode 100644 apps/core/priv/repo/seeds/012_enterprise_plan.exs diff --git a/apps/core/lib/core/schema/account.ex b/apps/core/lib/core/schema/account.ex index e05886f07..3496be83e 100644 --- a/apps/core/lib/core/schema/account.ex +++ b/apps/core/lib/core/schema/account.ex @@ -14,6 +14,7 @@ defmodule Core.Schema.Account do field :user_count, :integer, default: 0 field :cluster_count, :integer, default: 0 field :usage_updated, :boolean + field :sa_provisioned, :boolean belongs_to :root_user, User has_many :domain_mappings, DomainMapping, on_replace: :delete @@ -39,7 +40,7 @@ defmodule Core.Schema.Account do ) end - @valid ~w(name workos_connection_id)a + @valid ~w(name workos_connection_id sa_provisioned)a @payment ~w(billing_customer_id)a def changeset(model, attrs \\ %{}) do diff --git a/apps/core/lib/core/schema/platform_plan.ex b/apps/core/lib/core/schema/platform_plan.ex index a6e6d4dba..5e07269e1 100644 --- a/apps/core/lib/core/schema/platform_plan.ex +++ b/apps/core/lib/core/schema/platform_plan.ex @@ -28,6 +28,7 @@ defmodule Core.Schema.PlatformPlan do schema "platform_plans" do field :name, :string field :visible, :boolean, default: true + field :enterprise, :boolean field :cost, :integer field :period, Period field :external_id, :string diff --git a/apps/core/lib/core/services/accounts.ex b/apps/core/lib/core/services/accounts.ex index 1b770e0d0..9393b13fa 100644 --- a/apps/core/lib/core/services/accounts.ex +++ b/apps/core/lib/core/services/accounts.ex @@ -32,6 +32,8 @@ defmodule Core.Services.Accounts do def get_group!(id), do: Core.Repo.get!(Group, id) + def get_group_by_name(aid, name), do: Core.Repo.get_by(Group, name: name, account_id: aid) + def get_invite!(id), do: Core.Repo.get_by!(Invite, secure_id: id) def get_invite(id), do: Core.Repo.get_by(Invite, secure_id: id) @@ -173,6 +175,11 @@ defmodule Core.Services.Accounts do |> allow(user, :create) |> when_ok(:insert) end) + |> add_operation(:account, fn %{sa: %{account_id: aid}} -> {:ok, get_account!(aid)} end) + |> add_operation(:provision, fn + %{account: %Account{sa_provisioned: true} = account, sa: sa} -> setup_sa_group(account, sa) + %{account: account, sa: sa} -> provision_service_account_role(account, sa) + end) |> add_operation(:validate, fn %{sa: %{account_id: aid} = sa} -> case Core.Repo.preload(sa, impersonation_policy: :bindings) do %{impersonation_policy: %{bindings: bindings}} -> validate_bindings(aid, bindings) @@ -183,6 +190,48 @@ defmodule Core.Services.Accounts do |> Users.notify(:create) end + @sa_group "service-accounts" + + defp provision_service_account_role(%Account{id: aid} = account, %User{} = sa) do + start_transaction() + |> add_operation(:group, fn _ -> setup_sa_group(account, sa) end) + |> add_operation(:role, fn %{group: %{group: g}} -> + %Role{account_id: aid} + |> Role.changeset(%{ + name: @sa_group, + description: "permissions given to service accounts in your account", + repositories: ["*"], + permissions: %{install: true}, + role_bindings: [%{group_id: g.id}] + }) + |> Core.Repo.insert() + end) + |> add_operation(:account, fn _ -> + Account.changeset(account, %{sa_provisioned: true}) + |> Core.Repo.update() + end) + |> execute() + end + + defp setup_sa_group(%Account{id: aid}, %User{id: user_id}) do + start_transaction() + |> add_operation(:group, fn _ -> + case get_group_by_name(aid, @sa_group) do + %Group{} = g -> {:ok, g} + _ -> + %Group{account_id: aid} + |> Group.changeset(%{name: @sa_group, description: "group managed automatically for all service accounts"}) + |> Core.Repo.insert() + end + end) + |> add_operation(:group_member, fn %{group: group} -> + %GroupMember{group_id: group.id} + |> GroupMember.changeset(%{user_id: user_id}) + |> Core.Repo.insert() + end) + |> execute() + end + @doc """ Updates a service account """ diff --git a/apps/core/lib/core/services/payments.ex b/apps/core/lib/core/services/payments.ex index 0230e5087..bc5217428 100644 --- a/apps/core/lib/core/services/payments.ex +++ b/apps/core/lib/core/services/payments.ex @@ -139,6 +139,7 @@ defmodule Core.Services.Payments do {false, _, _, _} -> true {_, true, _, _} -> false {_, _, true, _} -> true + {_, _, _, %User{account: %Account{subscription: %PlatformSubscription{plan: %PlatformPlan{enterprise: true}}}}} -> true {_, _, _, %User{account: %Account{subscription: %PlatformSubscription{plan: %PlatformPlan{features: %{^feature => true}}}}}} -> true _ -> false end @@ -333,6 +334,7 @@ defmodule Core.Services.Payments do name: "Enterprise", period: :monthly, visible: true, + enterprise: true, features: %{vpn: true, user_management: true}, line_items: [ %{name: "User", dimension: :user, period: :monthly, cost: 4900}, # these costs are arbitrary, as won't be billed through stripe diff --git a/apps/core/priv/repo/migrations/20230109223232_add_role_provisioned_bool.exs b/apps/core/priv/repo/migrations/20230109223232_add_role_provisioned_bool.exs new file mode 100644 index 000000000..21da56936 --- /dev/null +++ b/apps/core/priv/repo/migrations/20230109223232_add_role_provisioned_bool.exs @@ -0,0 +1,13 @@ +defmodule Core.Repo.Migrations.AddRoleProvisionedBool do + use Ecto.Migration + + def change do + alter table(:accounts) do + add :sa_provisioned, :boolean + end + + alter table(:platform_plans) do + add :enterprise, :boolean + end + end +end diff --git a/apps/core/priv/repo/seeds/012_enterprise_plan.exs b/apps/core/priv/repo/seeds/012_enterprise_plan.exs new file mode 100644 index 000000000..22e91cd54 --- /dev/null +++ b/apps/core/priv/repo/seeds/012_enterprise_plan.exs @@ -0,0 +1,8 @@ +import Botanist + +seed do + enterprise = Core.Services.Payments.get_platform_plan_by_name!("Enterprise") + + Ecto.Changeset.change(enterprise, %{enterprise: true}) + |> Core.Repo.update!() +end diff --git a/apps/core/test/services/accounts_test.exs b/apps/core/test/services/accounts_test.exs index cd249ca9d..31acc14ab 100644 --- a/apps/core/test/services/accounts_test.exs +++ b/apps/core/test/services/accounts_test.exs @@ -22,6 +22,15 @@ defmodule Core.Services.AccountsTest do assert binding.group_id == group.id + %{groups: [group], group_role_bindings: [%{role: role}]} = Core.Services.Rbac.preload(srv_acct) + + assert group.name == "service-accounts" + assert group.account_id == srv_acct.account_id + assert role.name == "service-accounts" + assert role.permissions.install + + assert refetch(user.account).sa_provisioned + assert_receive {:event, %PubSub.UserCreated{item: ^srv_acct}} end diff --git a/apps/core/test/services/payments_test.exs b/apps/core/test/services/payments_test.exs index de612c399..863dbc9af 100644 --- a/apps/core/test/services/payments_test.exs +++ b/apps/core/test/services/payments_test.exs @@ -546,6 +546,13 @@ defmodule Core.Services.PaymentsTest do assert Payments.has_feature?(user, :user_management) end + test "if a user's plan is enterprise, it get's any feature" do + account = insert(:account) + insert(:platform_subscription, account: account, plan: build(:platform_plan, enterprise: true, features: %{user_management: false})) + user = insert(:user, account: account) + assert Payments.has_feature?(user, :user_management) + end + test "if a user's account is grandfathered, then it returns true" do account = insert(:account, grandfathered_until: Timex.now() |> Timex.shift(days: 1)) insert(:platform_subscription, account: account, plan: build(:platform_plan, features: %{user_management: false})) diff --git a/apps/graphql/lib/graphql/schema/payments.ex b/apps/graphql/lib/graphql/schema/payments.ex index 4f37709a1..de9af0129 100644 --- a/apps/graphql/lib/graphql/schema/payments.ex +++ b/apps/graphql/lib/graphql/schema/payments.ex @@ -99,6 +99,7 @@ defmodule GraphQl.Schema.Payments do field :visible, non_null(:boolean) field :cost, non_null(:integer) field :period, non_null(:payment_period) + field :enterprise, :boolean field :features, :plan_features field :line_items, list_of(:platform_plan_item)