diff --git a/elixir/apps/api/lib/api/device/channel.ex b/elixir/apps/api/lib/api/device/channel.ex index ef18e2488..166562f3d 100644 --- a/elixir/apps/api/lib/api/device/channel.ex +++ b/elixir/apps/api/lib/api/device/channel.ex @@ -85,6 +85,7 @@ defmodule API.Device.Channel do connected_gateway_ids = Map.get(attrs, "connected_gateway_ids", []) with {:ok, resource} <- Resources.fetch_resource_by_id(resource_id, socket.assigns.subject), + # TODO: # :ok = Resource.authorize(resource, socket.assigns.subject), {:ok, [_ | _] = gateways} <- Gateways.list_connected_gateways_for_resource(resource), diff --git a/elixir/apps/api/test/api/device/channel_test.exs b/elixir/apps/api/test/api/device/channel_test.exs index 0c4166bfc..0763b60e7 100644 --- a/elixir/apps/api/test/api/device/channel_test.exs +++ b/elixir/apps/api/test/api/device/channel_test.exs @@ -1,29 +1,27 @@ defmodule API.Device.ChannelTest do use API.ChannelCase - alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures, ResourcesFixtures} - alias Domain.{ConfigFixtures, DevicesFixtures, RelaysFixtures, GatewaysFixtures} setup do - account = AccountsFixtures.create_account() - ConfigFixtures.upsert_configuration(account: account, devices_upstream_dns: ["1.1.1.1"]) - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(actor: actor, account: account) - subject = AuthFixtures.create_subject(identity) - device = DevicesFixtures.create_device(subject: subject) - gateway = GatewaysFixtures.create_gateway(account: account) + account = Fixtures.Accounts.create_account() + Fixtures.Config.upsert_configuration(account: account, devices_upstream_dns: ["1.1.1.1"]) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(actor: actor, account: account) + subject = Fixtures.Auth.create_subject(identity: identity) + device = Fixtures.Devices.create_device(subject: subject) + gateway = Fixtures.Gateways.create_gateway(account: account) dns_resource = - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: gateway.group_id}] + connections: [%{gateway_group_id: gateway.group_id}] ) cidr_resource = - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( type: :cidr, address: "192.168.1.1/28", account: account, - gateway_groups: [%{gateway_group_id: gateway.group_id}] + connections: [%{gateway_group_id: gateway.group_id}] ) expires_at = DateTime.utc_now() |> DateTime.add(30, :second) @@ -138,7 +136,7 @@ defmodule API.Device.ChannelTest do dns_resource: resource, socket: socket } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) :ok = Domain.Gateways.connect_gateway(gateway) ref = push(socket, "prepare_connection", %{"resource_id" => resource.id}) @@ -152,9 +150,9 @@ defmodule API.Device.ChannelTest do socket: socket } do # Online Relay - global_relay_group = RelaysFixtures.create_global_group() - global_relay = RelaysFixtures.create_relay(group: global_relay_group, ipv6: nil) - relay = RelaysFixtures.create_relay(account: account) + global_relay_group = Fixtures.Relays.create_global_group() + global_relay = Fixtures.Relays.create_relay(group: global_relay_group, ipv6: nil) + relay = Fixtures.Relays.create_relay(account: account) stamp_secret = Ecto.UUID.generate() :ok = Domain.Relays.connect_relay(relay, stamp_secret) @@ -247,7 +245,7 @@ defmodule API.Device.ChannelTest do dns_resource: resource, socket: socket } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) :ok = Domain.Gateways.connect_gateway(gateway) attrs = %{ @@ -334,7 +332,7 @@ defmodule API.Device.ChannelTest do dns_resource: resource, socket: socket } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) :ok = Domain.Gateways.connect_gateway(gateway) attrs = %{ diff --git a/elixir/apps/api/test/api/device/socket_test.exs b/elixir/apps/api/test/api/device/socket_test.exs index 80d38e384..0883fa11a 100644 --- a/elixir/apps/api/test/api/device/socket_test.exs +++ b/elixir/apps/api/test/api/device/socket_test.exs @@ -3,7 +3,6 @@ defmodule API.Device.SocketTest do import API.Device.Socket, only: [id: 1] alias API.Device.Socket alias Domain.Auth - alias Domain.{AuthFixtures, DevicesFixtures} @connect_info %{ user_agent: "iOS/12.7 (iPhone) connlib/0.1.1", @@ -22,7 +21,7 @@ defmodule API.Device.SocketTest do end test "creates a new device" do - subject = AuthFixtures.create_subject() + subject = Fixtures.Auth.create_subject() {:ok, token} = Auth.create_session_token_from_subject(subject) attrs = connect_attrs(token: token) @@ -38,8 +37,8 @@ defmodule API.Device.SocketTest do end test "updates existing device" do - subject = AuthFixtures.create_subject() - existing_device = DevicesFixtures.create_device(subject: subject) + subject = Fixtures.Auth.create_subject() + existing_device = Fixtures.Devices.create_device(subject: subject) {:ok, token} = Auth.create_session_token_from_subject(subject) attrs = connect_attrs(token: token, external_id: existing_device.external_id) @@ -52,7 +51,7 @@ defmodule API.Device.SocketTest do describe "id/1" do test "creates a channel for a device" do - device = DevicesFixtures.create_device() + device = Fixtures.Devices.create_device() socket = socket(API.Device.Socket, "", %{device: device}) assert id(socket) == "device:#{device.id}" @@ -68,7 +67,7 @@ defmodule API.Device.SocketTest do end defp connect_attrs(attrs) do - DevicesFixtures.device_attrs() + Fixtures.Devices.device_attrs() |> Map.take(~w[external_id public_key]a) |> Map.merge(Enum.into(attrs, %{})) |> Enum.into(%{}, fn {k, v} -> {to_string(k), v} end) diff --git a/elixir/apps/api/test/api/gateway/channel_test.exs b/elixir/apps/api/test/api/gateway/channel_test.exs index 673e93502..b7f155b9f 100644 --- a/elixir/apps/api/test/api/gateway/channel_test.exs +++ b/elixir/apps/api/test/api/gateway/channel_test.exs @@ -1,20 +1,18 @@ defmodule API.Gateway.ChannelTest do use API.ChannelCase - alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures, ResourcesFixtures} - alias Domain.{DevicesFixtures, RelaysFixtures, GatewaysFixtures} setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(actor: actor, account: account) - subject = AuthFixtures.create_subject(identity) - device = DevicesFixtures.create_device(subject: subject) - gateway = GatewaysFixtures.create_gateway(account: account) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(actor: actor, account: account) + subject = Fixtures.Auth.create_subject(identity: identity) + device = Fixtures.Devices.create_device(subject: subject) + gateway = Fixtures.Gateways.create_gateway(account: account) resource = - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: gateway.group_id}] + connections: [%{gateway_group_id: gateway.group_id}] ) {:ok, _, socket} = @@ -22,7 +20,7 @@ defmodule API.Gateway.ChannelTest do |> socket("gateway:#{gateway.id}", %{gateway: gateway}) |> subscribe_and_join(API.Gateway.Channel, "gateway") - relay = RelaysFixtures.create_relay(account: account) + relay = Fixtures.Relays.create_relay(account: account) %{ account: account, diff --git a/elixir/apps/api/test/api/gateway/socket_test.exs b/elixir/apps/api/test/api/gateway/socket_test.exs index e8fea13b3..54ee9de4a 100644 --- a/elixir/apps/api/test/api/gateway/socket_test.exs +++ b/elixir/apps/api/test/api/gateway/socket_test.exs @@ -3,7 +3,6 @@ defmodule API.Gateway.SocketTest do import API.Gateway.Socket, except: [connect: 3] alias API.Gateway.Socket alias Domain.Gateways - alias Domain.GatewaysFixtures @connlib_version "0.1.1" @@ -19,7 +18,7 @@ defmodule API.Gateway.SocketTest do end test "creates a new gateway" do - token = GatewaysFixtures.create_token() + token = Fixtures.Gateways.create_token() encrypted_secret = Gateways.encode_token!(token) attrs = connect_attrs(token: encrypted_secret) @@ -35,8 +34,8 @@ defmodule API.Gateway.SocketTest do end test "updates existing gateway" do - token = GatewaysFixtures.create_token() - existing_gateway = GatewaysFixtures.create_gateway(token: token) + token = Fixtures.Gateways.create_token() + existing_gateway = Fixtures.Gateways.create_gateway(token: token) encrypted_secret = Gateways.encode_token!(token) attrs = connect_attrs(token: encrypted_secret, external_id: existing_gateway.external_id) @@ -54,7 +53,7 @@ defmodule API.Gateway.SocketTest do describe "id/1" do test "creates a channel for a gateway" do - gateway = GatewaysFixtures.create_gateway() + gateway = Fixtures.Gateways.create_gateway() socket = socket(API.Gateway.Socket, "", %{gateway: gateway}) assert id(socket) == "gateway:#{gateway.id}" @@ -62,7 +61,7 @@ defmodule API.Gateway.SocketTest do end defp connect_attrs(attrs) do - GatewaysFixtures.gateway_attrs() + Fixtures.Gateways.gateway_attrs() |> Map.take(~w[external_id public_key]a) |> Map.merge(Enum.into(attrs, %{})) |> Enum.into(%{}, fn {k, v} -> {to_string(k), v} end) diff --git a/elixir/apps/api/test/api/relay/channel_test.exs b/elixir/apps/api/test/api/relay/channel_test.exs index cf74ee833..17588268a 100644 --- a/elixir/apps/api/test/api/relay/channel_test.exs +++ b/elixir/apps/api/test/api/relay/channel_test.exs @@ -1,9 +1,8 @@ defmodule API.Relay.ChannelTest do use API.ChannelCase - alias Domain.RelaysFixtures setup do - relay = RelaysFixtures.create_relay() + relay = Fixtures.Relays.create_relay() stamp_secret = Domain.Crypto.rand_string() @@ -24,8 +23,8 @@ defmodule API.Relay.ChannelTest do end test "tracks presence after join of an global relay" do - group = RelaysFixtures.create_global_group() - relay = RelaysFixtures.create_relay(group: group) + group = Fixtures.Relays.create_global_group() + relay = Fixtures.Relays.create_relay(group: group) stamp_secret = Domain.Crypto.rand_string() diff --git a/elixir/apps/api/test/api/relay/socket_test.exs b/elixir/apps/api/test/api/relay/socket_test.exs index 66ede5ec2..b9a0a50fa 100644 --- a/elixir/apps/api/test/api/relay/socket_test.exs +++ b/elixir/apps/api/test/api/relay/socket_test.exs @@ -3,7 +3,6 @@ defmodule API.Relay.SocketTest do import API.Relay.Socket, except: [connect: 3] alias API.Relay.Socket alias Domain.Relays - alias Domain.RelaysFixtures @connlib_version "0.1.1" @@ -19,7 +18,7 @@ defmodule API.Relay.SocketTest do end test "creates a new relay" do - token = RelaysFixtures.create_token() + token = Fixtures.Relays.create_token() encrypted_secret = Relays.encode_token!(token) attrs = connect_attrs(token: encrypted_secret) @@ -35,8 +34,8 @@ defmodule API.Relay.SocketTest do end test "updates existing relay" do - token = RelaysFixtures.create_token() - existing_relay = RelaysFixtures.create_relay(token: token) + token = Fixtures.Relays.create_token() + existing_relay = Fixtures.Relays.create_relay(token: token) encrypted_secret = Relays.encode_token!(token) attrs = connect_attrs(token: encrypted_secret, ipv4: existing_relay.ipv4) @@ -54,7 +53,7 @@ defmodule API.Relay.SocketTest do describe "id/1" do test "creates a channel for a relay" do - relay = RelaysFixtures.create_relay() + relay = Fixtures.Relays.create_relay() socket = socket(API.Relay.Socket, "", %{relay: relay}) assert id(socket) == "relay:#{relay.id}" @@ -62,7 +61,7 @@ defmodule API.Relay.SocketTest do end defp connect_attrs(attrs) do - RelaysFixtures.relay_attrs() + Fixtures.Relays.relay_attrs() |> Map.take(~w[ipv4 ipv6]a) |> Map.merge(Enum.into(attrs, %{})) |> Enum.into(%{}, fn {k, v} -> {to_string(k), v} end) diff --git a/elixir/apps/api/test/support/channel_case.ex b/elixir/apps/api/test/support/channel_case.ex index 8b558cf2a..ff0a87e70 100644 --- a/elixir/apps/api/test/support/channel_case.ex +++ b/elixir/apps/api/test/support/channel_case.ex @@ -14,6 +14,7 @@ defmodule API.ChannelCase do import Phoenix.ChannelTest import API.ChannelCase alias Domain.Repo + alias Domain.Fixtures # The default endpoint for testing @endpoint API.Endpoint diff --git a/elixir/apps/domain/lib/domain/accounts.ex b/elixir/apps/domain/lib/domain/accounts.ex index c7c84be61..616a61cb7 100644 --- a/elixir/apps/domain/lib/domain/accounts.ex +++ b/elixir/apps/domain/lib/domain/accounts.ex @@ -50,7 +50,7 @@ defmodule Domain.Accounts do end def create_account(attrs) do - Account.Changeset.create_changeset(attrs) + Account.Changeset.create(attrs) |> Repo.insert() end diff --git a/elixir/apps/domain/lib/domain/accounts/account.ex b/elixir/apps/domain/lib/domain/accounts/account.ex index 3490bf357..8fd2addc1 100644 --- a/elixir/apps/domain/lib/domain/accounts/account.ex +++ b/elixir/apps/domain/lib/domain/accounts/account.ex @@ -5,6 +5,32 @@ defmodule Domain.Accounts.Account do field :name, :string field :slug, :string + # We mention all schemas here to leverage Ecto compile-time reference checks, + # because later we will have to shard data by account_id. + has_many :actors, Domain.Actors.Actor, where: [deleted_at: nil] + has_many :actor_group_memberships, Domain.Actors.Membership, where: [deleted_at: nil] + has_many :actor_groups, Domain.Actors.Group, where: [deleted_at: nil] + + has_many :auth_providers, Domain.Auth.Provider, where: [deleted_at: nil] + has_many :auth_identities, Domain.Auth.Identity, where: [deleted_at: nil] + + has_many :network_addresses, Domain.Network.Address, where: [deleted_at: nil] + + has_many :policies, Domain.Policies.Policy, where: [deleted_at: nil] + + has_many :resources, Domain.Resources.Resource, where: [deleted_at: nil] + has_many :resource_connections, Domain.Resources.Connection, where: [deleted_at: nil] + + has_many :devices, Domain.Devices.Device, where: [deleted_at: nil] + + has_many :gateways, Domain.Gateways.Gateway, where: [deleted_at: nil] + has_many :gateway_groups, Domain.Gateways.Group, where: [deleted_at: nil] + has_many :gateway_tokens, Domain.Gateways.Token, where: [deleted_at: nil] + + has_many :relays, Domain.Relays.Relay, where: [deleted_at: nil] + has_many :relay_groups, Domain.Relays.Group, where: [deleted_at: nil] + has_many :relay_tokens, Domain.Relays.Token, where: [deleted_at: nil] + timestamps() end end diff --git a/elixir/apps/domain/lib/domain/accounts/account/changeset.ex b/elixir/apps/domain/lib/domain/accounts/account/changeset.ex index a2687d7bb..f851948b6 100644 --- a/elixir/apps/domain/lib/domain/accounts/account/changeset.ex +++ b/elixir/apps/domain/lib/domain/accounts/account/changeset.ex @@ -2,28 +2,23 @@ defmodule Domain.Accounts.Account.Changeset do use Domain, :changeset alias Domain.Accounts.Account - def changeset(account, attrs) do - account + def create(attrs) do + %Account{} |> cast(attrs, [:name, :slug]) + |> changeset() + end + + defp changeset(changeset) do + changeset |> validate_required([:name]) - |> validate_name() |> trim_change(:name) + |> validate_length(:name, min: 3, max: 64) |> prepare_changes(fn changeset -> put_slug_default(changeset) end) |> downcase_slug() |> validate_slug() |> unique_constraint(:slug, name: :accounts_slug_index) end - def create_changeset(attrs) do - %Account{} - |> changeset(attrs) - end - - defp validate_name(changeset) do - changeset - |> validate_length(:name, min: 3, max: 64) - end - defp put_slug_default(changeset) do changeset |> put_default_value(:slug, &Domain.Accounts.generate_unique_slug/0) diff --git a/elixir/apps/domain/lib/domain/actors.ex b/elixir/apps/domain/lib/domain/actors.ex index 855841eb8..e3fc3a90a 100644 --- a/elixir/apps/domain/lib/domain/actors.ex +++ b/elixir/apps/domain/lib/domain/actors.ex @@ -1,117 +1,12 @@ defmodule Domain.Actors do - alias Domain.{Repo, Auth, Validator} + alias Domain.Actors.Membership + alias Web.Devices + alias Domain.{Repo, Validator} + alias Domain.{Accounts, Auth, Devices} alias Domain.Actors.{Authorizer, Actor, Group} require Ecto.Query - def fetch_group_by_id(id, %Auth.Subject{} = subject) do - with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()), - true <- Validator.valid_uuid?(id) do - Group.Query.by_id(id) - |> Authorizer.for_subject(subject) - |> Repo.fetch() - else - false -> {:error, :not_found} - other -> other - end - end - - def list_groups(%Auth.Subject{} = subject) do - with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do - Group.Query.all() - |> Authorizer.for_subject(subject) - |> Repo.list() - end - end - - def new_group(attrs \\ %{}) do - change_group(%Group{}, attrs) - end - - def upsert_provider_groups(%Auth.Provider{} = provider, attrs_by_provider_identifier) do - attrs_by_provider_identifier - |> Enum.reduce(Ecto.Multi.new(), fn {provider_identifier, attrs}, multi -> - Ecto.Multi.insert( - multi, - {:group, provider_identifier}, - Group.Changeset.create_changeset(provider, provider_identifier, attrs), - conflict_target: Group.Changeset.upsert_conflict_target(), - on_conflict: Group.Changeset.upsert_on_conflict(), - returning: true - ) - end) - |> Repo.transaction() - - # Ecto.Multi.new() - # |> Ecto.Multi.insert(:actor, Actor.Changeset.create_changeset(provider, attrs)) - # |> Ecto.Multi.run(:identity, fn _repo, %{actor: actor} -> - # Auth.create_identity(actor, provider, provider_identifier) - # end) - # |> Repo.transaction() - |> case do - {:ok, %{group: group}} -> - {:ok, group} - - {:error, _step, changeset, _effects_so_far} -> - {:error, changeset} - end - end - - def group_synced?(%Group{provider_id: nil}), do: false - def group_synced?(%Group{}), do: true - - def create_group(attrs, %Auth.Subject{} = subject) do - with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do - subject.account - |> Group.Changeset.create_changeset(attrs) - |> Repo.insert() - end - end - - def change_group(group, attrs \\ %{}) - - def change_group(%Group{provider_id: nil} = group, attrs) do - group - |> Repo.preload(:memberships) - |> Group.Changeset.update_changeset(attrs) - end - - def change_group(%Group{}, _attrs) do - raise ArgumentError, "can't change synced groups" - end - - def update_group(%Group{provider_id: nil} = group, attrs, %Auth.Subject{} = subject) do - with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do - group - |> Repo.preload(:memberships) - |> Group.Changeset.update_changeset(attrs) - |> Repo.update() - end - end - - def update_group(%Group{}, _attrs, %Auth.Subject{}) do - {:error, :synced_group} - end - - def delete_group(%Group{provider_id: nil} = group, %Auth.Subject{} = subject) do - with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do - Group.Query.by_id(group.id) - |> Authorizer.for_subject(subject) - |> Group.Query.by_account_id(subject.account.id) - |> Repo.fetch_and_update(with: &Group.Changeset.delete_changeset/1) - end - end - - def delete_group(%Group{}, %Auth.Subject{}) do - {:error, :synced_group} - end - - def fetch_count_by_type(type, %Auth.Subject{} = subject) do - with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do - Actor.Query.by_type(type) - |> Authorizer.for_subject(subject) - |> Repo.aggregate(:count) - end - end + # Groups def fetch_groups_count_grouped_by_provider_id(%Auth.Subject{} = subject) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do @@ -129,11 +24,136 @@ defmodule Domain.Actors do end end - def fetch_actor_by_id(id, %Auth.Subject{} = subject, opts \\ []) do - {preload, _opts} = Keyword.pop(opts, :preload, []) - + def fetch_group_by_id(id, %Auth.Subject{} = subject, opts \\ []) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()), true <- Validator.valid_uuid?(id) do + {preload, _opts} = Keyword.pop(opts, :preload, []) + + Group.Query.by_id(id) + |> Authorizer.for_subject(subject) + |> Repo.fetch() + |> case do + {:ok, group} -> {:ok, Repo.preload(group, preload)} + {:error, reason} -> {:error, reason} + end + else + false -> {:error, :not_found} + other -> other + end + end + + def list_groups(%Auth.Subject{} = subject, opts \\ []) do + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do + {preload, _opts} = Keyword.pop(opts, :preload, []) + + {:ok, groups} = + Group.Query.all() + |> Authorizer.for_subject(subject) + |> Repo.list() + + {:ok, Repo.preload(groups, preload)} + end + end + + def peek_group_actors(groups, limit, %Auth.Subject{} = subject) do + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do + ids = groups |> Enum.map(& &1.id) |> Enum.uniq() + + Group.Query.by_id({:in, ids}) + |> Group.Query.preload_few_actors_for_each_group(limit) + |> Repo.peek(groups) + end + end + + def peek_actor_groups(actors, limit, %Auth.Subject{} = subject) do + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do + ids = actors |> Enum.map(& &1.id) |> Enum.uniq() + + Actor.Query.by_id({:in, ids}) + |> Actor.Query.preload_few_groups_for_each_actor(limit) + |> Repo.peek(actors) + end + end + + def sync_provider_groups_multi(%Auth.Provider{} = provider, attrs_list) do + Group.Sync.sync_provider_groups_multi(provider, attrs_list) + end + + def sync_provider_memberships_multi(multi, %Auth.Provider{} = provider, tuples) do + Membership.Sync.sync_provider_memberships_multi(multi, provider, tuples) + end + + def new_group(attrs \\ %{}) do + change_group(%Group{}, attrs) + end + + def create_group(attrs, %Auth.Subject{} = subject) do + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do + subject.account + |> Group.Changeset.create(attrs, subject) + |> Repo.insert() + end + end + + def change_group(group, attrs \\ %{}) + + def change_group(%Group{provider_id: nil} = group, attrs) do + group + |> Repo.preload(:memberships) + |> Group.Changeset.update(attrs) + end + + def change_group(%Group{}, _attrs) do + raise ArgumentError, "can't change synced groups" + end + + def update_group(%Group{provider_id: nil} = group, attrs, %Auth.Subject{} = subject) do + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do + group + |> Repo.preload(:memberships) + |> Group.Changeset.update(attrs) + |> Repo.update() + end + end + + def update_group(%Group{}, _attrs, %Auth.Subject{}) do + {:error, :synced_group} + end + + def delete_group(%Group{provider_id: nil} = group, %Auth.Subject{} = subject) do + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do + Group.Query.by_id(group.id) + |> Authorizer.for_subject(subject) + |> Group.Query.by_account_id(subject.account.id) + |> Repo.fetch_and_update(with: &Group.Changeset.delete/1) + end + end + + def delete_group(%Group{}, %Auth.Subject{}) do + {:error, :synced_group} + end + + def group_synced?(%Group{provider_id: nil}), do: false + def group_synced?(%Group{}), do: true + + def group_deleted?(%Group{deleted_at: nil}), do: false + def group_deleted?(%Group{}), do: true + + # Actors + + def fetch_actors_count_by_type(type, %Auth.Subject{} = subject) do + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do + Actor.Query.by_type(type) + |> Authorizer.for_subject(subject) + |> Repo.aggregate(:count) + end + end + + def fetch_actor_by_id(id, %Auth.Subject{} = subject, opts \\ []) do + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()), + true <- Validator.valid_uuid?(id) do + {preload, _opts} = Keyword.pop(opts, :preload, []) + Actor.Query.by_id(id) |> Authorizer.for_subject(subject) |> Repo.fetch() @@ -163,76 +183,52 @@ defmodule Domain.Actors do def list_actors(%Auth.Subject{} = subject, opts \\ []) do {preload, _opts} = Keyword.pop(opts, :preload, []) - {hydrate, _opts} = Keyword.pop(opts, :hydrate, []) with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do {:ok, actors} = Actor.Query.all() |> Authorizer.for_subject(subject) - |> hydrate_fields(hydrate) |> Repo.list() {:ok, Repo.preload(actors, preload)} end end - defp hydrate_fields(queryable, []), do: queryable + def new_actor(attrs \\ %{memberships: []}) do + Actor.Changeset.create(attrs) + end - def create_actor( - %Auth.Provider{} = provider, - provider_identifier, - attrs, - %Auth.Subject{} = subject - ) do + def create_actor(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()), - :ok <- Auth.ensure_has_access_to(subject, provider), - changeset = Actor.Changeset.create_changeset(provider.account_id, attrs), - {:ok, data} <- Ecto.Changeset.apply_action(changeset, :validate) do - granted_permissions = Auth.fetch_type_permissions!(data.type) - - if MapSet.subset?(granted_permissions, subject.permissions) do - create_actor(provider, provider_identifier, attrs) - else - missing_permissions = - MapSet.difference(granted_permissions, subject.permissions) - |> MapSet.to_list() - - {:error, {:unauthorized, privilege_escalation: missing_permissions}} - end + :ok <- Accounts.ensure_has_access_to(subject, account) do + Actor.Changeset.create(account.id, attrs, subject) + |> Repo.insert() end end - def create_actor(%Auth.Provider{} = provider, provider_identifier, attrs) do - {provider_attrs, attrs} = Map.pop(attrs, "provider", %{}) - - Ecto.Multi.new() - |> Ecto.Multi.insert(:actor, Actor.Changeset.create_changeset(provider.account_id, attrs)) - |> Ecto.Multi.run(:identity, fn _repo, %{actor: actor} -> - Auth.create_identity(actor, provider, provider_identifier, provider_attrs) - end) - |> Repo.transaction() - |> case do - {:ok, %{actor: actor, identity: identity}} -> - {:ok, %{actor | identities: [identity]}} - - {:error, _step, changeset, _effects_so_far} -> - {:error, changeset} - end + def create_actor(%Accounts.Account{} = account, attrs) do + Actor.Changeset.create(account.id, attrs) + |> Repo.insert() end - def change_actor_type(%Actor{} = actor, type, %Auth.Subject{} = subject) do + def change_actor(%Actor{} = actor, attrs \\ %{}) do + Actor.Changeset.update(actor, attrs) + end + + def update_actor(%Actor{} = actor, attrs, %Auth.Subject{} = subject) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do Actor.Query.by_id(actor.id) |> Authorizer.for_subject(subject) |> Repo.fetch_and_update( with: fn actor -> - changeset = Actor.Changeset.set_actor_type(actor, type) + actor = Repo.preload(actor, :memberships) + changeset = Actor.Changeset.update(actor, attrs, subject) cond do - changeset.data.type != :admin -> + changeset.data.type != :account_admin_user -> changeset - changeset.changes.type == :admin -> + Map.get(changeset.changes, :type) == :account_admin_user -> changeset other_enabled_admins_exist?(actor) -> @@ -252,7 +248,7 @@ defmodule Domain.Actors do |> Authorizer.for_subject(subject) |> Repo.fetch_and_update( with: fn actor -> - if other_enabled_admins_exist?(actor) do + if actor.type != :account_admin_user or other_enabled_admins_exist?(actor) do Actor.Changeset.disable_actor(actor) else :cant_disable_the_last_admin @@ -276,7 +272,10 @@ defmodule Domain.Actors do |> Authorizer.for_subject(subject) |> Repo.fetch_and_update( with: fn actor -> - if other_enabled_admins_exist?(actor) do + if actor.type != :account_admin_user or other_enabled_admins_exist?(actor) do + :ok = Auth.delete_actor_identities(actor) + :ok = Devices.delete_actor_devices(actor) + Actor.Changeset.delete_actor(actor) else :cant_delete_the_last_admin @@ -286,6 +285,16 @@ defmodule Domain.Actors do end end + # TODO: when actor is synced we should not allow changing the name + def actor_synced?(%Actor{last_synced_at: nil}), do: false + def actor_synced?(%Actor{}), do: true + + def actor_deleted?(%Actor{deleted_at: nil}), do: false + def actor_deleted?(%Actor{}), do: true + + def actor_disabled?(%Actor{disabled_at: nil}), do: false + def actor_disabled?(%Actor{}), do: true + defp other_enabled_admins_exist?(%Actor{ type: :account_admin_user, account_id: account_id, diff --git a/elixir/apps/domain/lib/domain/actors/actor.ex b/elixir/apps/domain/lib/domain/actors/actor.ex index 9119e6a22..e84ff223a 100644 --- a/elixir/apps/domain/lib/domain/actors/actor.ex +++ b/elixir/apps/domain/lib/domain/actors/actor.ex @@ -7,12 +7,13 @@ defmodule Domain.Actors.Actor do field :name, :string has_many :identities, Domain.Auth.Identity, where: [deleted_at: nil] + has_many :devices, Domain.Devices.Device, where: [deleted_at: nil] + has_many :memberships, Domain.Actors.Membership, on_replace: :delete + has_many :groups, through: [:memberships, :group] belongs_to :account, Domain.Accounts.Account - has_many :memberships, Domain.Actors.Membership, on_replace: :delete - has_many :groups, through: [:memberships, :group], where: [deleted_at: nil] - + field :last_synced_at, :utc_datetime_usec field :disabled_at, :utc_datetime_usec field :deleted_at, :utc_datetime_usec timestamps() diff --git a/elixir/apps/domain/lib/domain/actors/actor/changeset.ex b/elixir/apps/domain/lib/domain/actors/actor/changeset.ex index 5c8908eaa..03e5db9d5 100644 --- a/elixir/apps/domain/lib/domain/actors/actor/changeset.ex +++ b/elixir/apps/domain/lib/domain/actors/actor/changeset.ex @@ -1,41 +1,84 @@ defmodule Domain.Actors.Actor.Changeset do use Domain, :changeset + alias Domain.Auth alias Domain.Actors + alias Domain.Actors.Actor - def changeset(actor, attrs) do - actor - |> cast(attrs, ~w[type name]a) - |> validate_required(~w[type name]a) - |> validate_length(:name, min: 1, max: 255) + def keys, do: ~w[type name]a + def keys(%Actor{last_synced_at: nil}), do: ~w[type name]a + def keys(%Actor{}), do: ~w[type]a + + def create(account_id, attrs, %Auth.Subject{} = subject) do + create(account_id, attrs) + |> validate_granted_permissions(subject) end - def create_changeset(account_id, attrs) do - %Actors.Actor{} - |> changeset(attrs) + def create(account_id, attrs) do + create(attrs) |> put_change(:account_id, account_id) + |> cast_assoc(:memberships, + with: &Actors.Membership.Changeset.changeset(account_id, &1, &2) + ) end - def set_actor_type(actor, type) do + def create(attrs) do + keys = keys() + + %Actors.Actor{memberships: []} + |> cast(attrs, keys) + |> validate_required(keys) + |> changeset() + end + + def update(%Actor{} = actor, attrs, %Auth.Subject{} = subject) do + update(actor, attrs) + |> validate_granted_permissions(subject) + end + + def update(%Actor{} = actor, attrs) do + keys = keys(actor) + actor - |> change() - |> put_change(:type, type) + |> cast(attrs, keys) + |> validate_required(keys) + |> cast_assoc(:memberships, + with: &Actors.Membership.Changeset.changeset(actor.account_id, &1, &2) + ) + |> changeset() end - def disable_actor(actor) do + def changeset(changeset) do + changeset + # Actor name can be very long in case IdP syncs something crazy long to us, + # we still don't wait to fail for that silently + |> validate_length(:name, max: 512) + end + + def disable_actor(%Actor{} = actor) do actor |> change() |> put_default_value(:disabled_at, DateTime.utc_now()) end - def enable_actor(actor) do + def enable_actor(%Actor{} = actor) do actor |> change() - |> put_default_value(:disabled_at, nil) + |> put_change(:disabled_at, nil) end - def delete_actor(actor) do + def delete_actor(%Actor{} = actor) do actor |> change() |> put_default_value(:deleted_at, DateTime.utc_now()) end + + defp validate_granted_permissions(changeset, subject) do + validate_change(changeset, :type, fn :type, granted_actor_type -> + if Auth.can_grant_role?(subject, granted_actor_type) do + [] + else + [{:type, "does not have permissions to grant this actor type"}] + end + end) + end end diff --git a/elixir/apps/domain/lib/domain/actors/actor/query.ex b/elixir/apps/domain/lib/domain/actors/actor/query.ex index dd603e243..2af9c5d91 100644 --- a/elixir/apps/domain/lib/domain/actors/actor/query.ex +++ b/elixir/apps/domain/lib/domain/actors/actor/query.ex @@ -8,6 +8,10 @@ defmodule Domain.Actors.Actor.Query do def by_id(queryable \\ all(), id) + def by_id(queryable, {:in, ids}) do + where(queryable, [actors: actors], actors.id in ^ids) + end + def by_id(queryable, {:not, id}) do where(queryable, [actors: actors], actors.id != ^id) end @@ -28,13 +32,58 @@ defmodule Domain.Actors.Actor.Query do where(queryable, [actors: actors], is_nil(actors.disabled_at)) end + def preload_few_groups_for_each_actor(queryable \\ all(), limit) do + queryable + |> with_joined_memberships(limit) + |> with_joined_groups() + |> with_joined_group_counts() + |> select([actors: actors, groups: groups, group_counts: group_counts], %{ + id: actors.id, + count: group_counts.count, + item: groups + }) + end + + def with_joined_memberships(queryable, limit) do + subquery = + Domain.Actors.Membership.Query.all() + |> where([memberships: memberships], memberships.actor_id == parent_as(:actors).id) + # we need second join to exclude soft deleted actors before applying a limit + |> join(:inner, [memberships: memberships], groups in ^Domain.Actors.Group.Query.all(), + on: groups.id == memberships.group_id + ) + |> select([memberships: memberships], memberships.group_id) + |> limit(^limit) + + join(queryable, :cross_lateral, [actors: actors], memberships in subquery(subquery), + as: :memberships + ) + end + + def with_joined_group_counts(queryable) do + subquery = + Domain.Actors.Membership.Query.count_groups_by_actor_id() + |> where([memberships: memberships], memberships.actor_id == parent_as(:actors).id) + + join(queryable, :cross_lateral, [actors: actors], group_counts in subquery(subquery), + as: :group_counts + ) + end + + def with_joined_groups(queryable \\ all()) do + join(queryable, :left, [memberships: memberships], groups in ^Domain.Actors.Group.Query.all(), + on: groups.id == memberships.group_id, + as: :groups + ) + end + def lock(queryable \\ all()) do lock(queryable, "FOR UPDATE") end - def with_assoc(queryable \\ all(), assoc) do + def with_assoc(queryable \\ all(), qual \\ :left, assoc) do with_named_binding(queryable, assoc, fn query, binding -> - join(query, :left, [actors: actors], a in assoc(actors, ^binding), as: ^binding) + join(query, qual, [actors: actors], a in assoc(actors, ^binding), as: ^binding) end) end end diff --git a/elixir/apps/domain/lib/domain/actors/group.ex b/elixir/apps/domain/lib/domain/actors/group.ex index 61d1daa9a..4dc3516e5 100644 --- a/elixir/apps/domain/lib/domain/actors/group.ex +++ b/elixir/apps/domain/lib/domain/actors/group.ex @@ -5,11 +5,18 @@ defmodule Domain.Actors.Group do field :name, :string # Those fields will be set for groups we synced from IdP's - belongs_to :provider, Domain.Auth.Provider + belongs_to :provider, Domain.Auth.Provider, where: [deleted_at: nil] field :provider_identifier, :string + has_many :policies, Domain.Policies.Policy, + foreign_key: :actor_group_id, + where: [deleted_at: nil] + has_many :memberships, Domain.Actors.Membership, on_replace: :delete - has_many :actors, through: [:memberships, :actor], where: [deleted_at: nil] + has_many :actors, through: [:memberships, :actor] + + field :created_by, Ecto.Enum, values: ~w[identity provider]a + belongs_to :created_by_identity, Domain.Auth.Identity belongs_to :account, Domain.Accounts.Account diff --git a/elixir/apps/domain/lib/domain/actors/group/changeset.ex b/elixir/apps/domain/lib/domain/actors/group/changeset.ex index 75f2ed87d..4192d5623 100644 --- a/elixir/apps/domain/lib/domain/actors/group/changeset.ex +++ b/elixir/apps/domain/lib/domain/actors/group/changeset.ex @@ -3,54 +3,55 @@ defmodule Domain.Actors.Group.Changeset do alias Domain.{Auth, Accounts} alias Domain.Actors - @fields ~w[name]a - def upsert_conflict_target do {:unsafe_fragment, "(account_id, provider_id, provider_identifier) " <> "WHERE deleted_at IS NULL AND provider_id IS NOT NULL AND provider_identifier IS NOT NULL"} end - # We do not update the `name` field because we allow to manually override it in the UI - # for usability reasons when the provider uses group names that can make people confused - def upsert_on_conflict, do: {:replace, (@fields -- ~w[name]a) ++ ~w[updated_at]a} + def upsert_on_conflict, do: {:replace, ~w[name updated_at]a} - def create_changeset(%Accounts.Account{} = account, attrs) do - %Actors.Group{account_id: account.id} - |> changeset(attrs) - |> validate_length(:name, min: 1, max: 64) - |> cast_assoc(:memberships, - with: &Actors.Membership.Changeset.group_changeset(account.id, &1, &2) - ) - end - - def create_changeset(%Auth.Provider{} = provider, provider_identifier, attrs) do + def create(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do %Actors.Group{} - |> changeset(attrs) + |> cast(attrs, ~w[name]a) + |> validate_required(~w[name]a) + |> changeset() + |> put_change(:account_id, account.id) + |> cast_assoc(:memberships, + with: &Actors.Membership.Changeset.changeset(account.id, &1, &2) + ) + |> put_change(:created_by, :identity) + |> put_change(:created_by_identity_id, subject.identity.id) + end + + def create(%Auth.Provider{} = provider, attrs) do + %Actors.Group{} + |> cast(attrs, ~w[name provider_identifier]a) + |> validate_required(~w[name provider_identifier]a) + |> changeset() |> put_change(:provider_id, provider.id) - |> put_change(:provider_identifier, provider_identifier) - |> cast_assoc(:memberships, - with: &Actors.Membership.Changeset.group_changeset(provider.account_id, &1, &2) - ) + |> put_change(:account_id, provider.account_id) + |> put_change(:created_by, :provider) end - def update_changeset(%Actors.Group{} = group, attrs) do - changeset(group, attrs) - |> validate_length(:name, min: 1, max: 64) - |> cast_assoc(:memberships, - with: &Actors.Membership.Changeset.group_changeset(group.account_id, &1, &2) - ) - end - - defp changeset(group, attrs) do + def update(%Actors.Group{} = group, attrs) do group - |> cast(attrs, @fields) - |> validate_required(@fields) + |> cast(attrs, ~w[name]a) + |> validate_required(~w[name]a) + |> changeset() + |> cast_assoc(:memberships, + with: &Actors.Membership.Changeset.changeset(group.account_id, &1, &2) + ) + end + + defp changeset(changeset) do + changeset |> trim_change(:name) + |> validate_length(:name, min: 1, max: 64) |> unique_constraint(:name, name: :actor_groups_account_id_name_index) end - def delete_changeset(%Actors.Group{} = group) do + def delete(%Actors.Group{} = group) do group |> change() |> put_default_value(:deleted_at, DateTime.utc_now()) diff --git a/elixir/apps/domain/lib/domain/actors/group/query.ex b/elixir/apps/domain/lib/domain/actors/group/query.ex index 09ddc7624..21d1a438e 100644 --- a/elixir/apps/domain/lib/domain/actors/group/query.ex +++ b/elixir/apps/domain/lib/domain/actors/group/query.ex @@ -6,7 +6,13 @@ defmodule Domain.Actors.Group.Query do |> where([groups: groups], is_nil(groups.deleted_at)) end - def by_id(queryable \\ all(), id) do + def by_id(queryable \\ all(), id) + + def by_id(queryable, {:in, ids}) do + where(queryable, [groups: groups], groups.id in ^ids) + end + + def by_id(queryable, id) do where(queryable, [groups: groups], groups.id == ^id) end @@ -18,7 +24,13 @@ defmodule Domain.Actors.Group.Query do where(queryable, [groups: groups], groups.provider_id == ^provider_id) end - def by_provider_identifier(queryable \\ all(), provider_identifier) do + def by_provider_identifier(queryable \\ all(), provider_identifier) + + def by_provider_identifier(queryable, {:in, provider_identifiers}) do + where(queryable, [groups: groups], groups.provider_identifier in ^provider_identifiers) + end + + def by_provider_identifier(queryable, provider_identifier) do where(queryable, [groups: groups], groups.provider_identifier == ^provider_identifier) end @@ -32,6 +44,57 @@ defmodule Domain.Actors.Group.Query do }) end + def preload_few_actors_for_each_group(queryable \\ all(), limit) do + queryable + |> with_joined_memberships(limit) + |> with_joined_actors() + |> with_joined_actor_counts() + |> select([groups: groups, actors: actors, actor_counts: actor_counts], %{ + id: groups.id, + count: actor_counts.count, + item: actors + }) + end + + def with_joined_memberships(queryable) do + join(queryable, :left, [groups: groups], memberships in assoc(groups, :memberships), + as: :memberships + ) + end + + def with_joined_memberships(queryable, limit) do + subquery = + Domain.Actors.Membership.Query.all() + |> where([memberships: memberships], memberships.group_id == parent_as(:groups).id) + # we need second join to exclude soft deleted actors before applying a limit + |> join(:inner, [memberships: memberships], actors in ^Domain.Actors.Actor.Query.all(), + on: actors.id == memberships.actor_id + ) + |> select([memberships: memberships], memberships.actor_id) + |> limit(^limit) + + join(queryable, :cross_lateral, [groups: groups], memberships in subquery(subquery), + as: :memberships + ) + end + + def with_joined_actor_counts(queryable) do + subquery = + Domain.Actors.Membership.Query.count_actors_by_group_id() + |> where([memberships: memberships], memberships.group_id == parent_as(:groups).id) + + join(queryable, :cross_lateral, [groups: groups], actor_counts in subquery(subquery), + as: :actor_counts + ) + end + + def with_joined_actors(queryable \\ all()) do + join(queryable, :left, [memberships: memberships], actors in ^Domain.Actors.Actor.Query.all(), + on: actors.id == memberships.actor_id, + as: :actors + ) + end + def lock(queryable \\ all()) do lock(queryable, "FOR UPDATE") end diff --git a/elixir/apps/domain/lib/domain/actors/group/sync.ex b/elixir/apps/domain/lib/domain/actors/group/sync.ex new file mode 100644 index 000000000..9dd246ea3 --- /dev/null +++ b/elixir/apps/domain/lib/domain/actors/group/sync.ex @@ -0,0 +1,82 @@ +defmodule Domain.Actors.Group.Sync do + alias Domain.Auth + alias Domain.Actors.Group + + def sync_provider_groups_multi(%Auth.Provider{} = provider, attrs_list) do + now = DateTime.utc_now() + + attrs_by_provider_identifier = + for attrs <- attrs_list, into: %{} do + {Map.fetch!(attrs, "provider_identifier"), attrs} + end + + Ecto.Multi.new() + |> Ecto.Multi.all(:groups, fn _effects_so_far -> + fetch_and_lock_provider_groups_query(provider) + end) + |> Ecto.Multi.run(:plan_groups, fn _repo, %{groups: groups} -> + plan_groups_update(groups, attrs_by_provider_identifier) + end) + |> Ecto.Multi.update_all( + :delete_groups, + fn %{plan_groups: {_upsert, delete}} -> + delete_groups_query(provider, delete) + end, + set: [deleted_at: now] + ) + |> Ecto.Multi.run(:upsert_groups, fn repo, %{plan_groups: {upsert, _delete}} -> + upsert_groups(repo, provider, attrs_by_provider_identifier, upsert) + end) + end + + defp fetch_and_lock_provider_groups_query(provider) do + Group.Query.by_account_id(provider.account_id) + |> Group.Query.by_provider_id(provider.id) + |> Group.Query.lock() + end + + defp plan_groups_update(groups, attrs_by_provider_identifier) do + {update, delete} = + Enum.reduce(groups, {[], []}, fn group, {update, delete} -> + if Map.has_key?(attrs_by_provider_identifier, group.provider_identifier) do + {[group.provider_identifier] ++ update, delete} + else + {update, [group.provider_identifier] ++ delete} + end + end) + + insert = Map.keys(attrs_by_provider_identifier) -- (update ++ delete) + + {:ok, {update ++ insert, delete}} + end + + defp delete_groups_query(provider, provider_identifiers_to_delete) do + Group.Query.by_account_id(provider.account_id) + |> Group.Query.by_provider_id(provider.id) + |> Group.Query.by_provider_identifier({:in, provider_identifiers_to_delete}) + end + + defp upsert_groups(repo, provider, attrs_by_provider_identifier, provider_identifiers_to_upsert) do + provider_identifiers_to_upsert + |> Enum.reduce_while({:ok, []}, fn provider_identifier, {:ok, acc} -> + attrs = Map.get(attrs_by_provider_identifier, provider_identifier) + + case upsert_group(repo, provider, attrs) do + {:ok, group} -> + {:cont, {:ok, [group | acc]}} + + {:error, changeset} -> + {:halt, {:error, changeset}} + end + end) + end + + defp upsert_group(repo, provider, attrs) do + Group.Changeset.create(provider, attrs) + |> repo.insert( + conflict_target: Group.Changeset.upsert_conflict_target(), + on_conflict: Group.Changeset.upsert_on_conflict(), + returning: true + ) + end +end diff --git a/elixir/apps/domain/lib/domain/actors/membership.ex b/elixir/apps/domain/lib/domain/actors/membership.ex index 6832df8a7..0f61f0a21 100644 --- a/elixir/apps/domain/lib/domain/actors/membership.ex +++ b/elixir/apps/domain/lib/domain/actors/membership.ex @@ -3,8 +3,8 @@ defmodule Domain.Actors.Membership do @primary_key false schema "actor_group_memberships" do - belongs_to :group, Domain.Actors.Group, primary_key: true - belongs_to :actor, Domain.Actors.Actor, primary_key: true + belongs_to :group, Domain.Actors.Group, primary_key: true, where: [deleted_at: nil] + belongs_to :actor, Domain.Actors.Actor, primary_key: true, where: [deleted_at: nil] belongs_to :account, Domain.Accounts.Account end diff --git a/elixir/apps/domain/lib/domain/actors/membership/changeset.ex b/elixir/apps/domain/lib/domain/actors/membership/changeset.ex index 6e63df91d..e14ad15b5 100644 --- a/elixir/apps/domain/lib/domain/actors/membership/changeset.ex +++ b/elixir/apps/domain/lib/domain/actors/membership/changeset.ex @@ -1,15 +1,13 @@ defmodule Domain.Actors.Membership.Changeset do use Domain, :changeset - def group_changeset(account_id, connection, attrs) do - connection - |> cast(attrs, ~w[actor_id]a) - |> validate_required(~w[actor_id]a) - |> changeset(account_id) - end + def upsert_conflict_target, do: [:group_id, :actor_id] + def upsert_on_conflict, do: :nothing - defp changeset(changeset, account_id) do - changeset + def changeset(account_id, membership, attrs) do + membership + |> cast(attrs, ~w[actor_id group_id]a) + |> validate_required_one_of(~w[actor_id group_id]a) |> assoc_constraint(:actor) |> assoc_constraint(:group) |> assoc_constraint(:account) diff --git a/elixir/apps/domain/lib/domain/actors/membership/query.ex b/elixir/apps/domain/lib/domain/actors/membership/query.ex index a08bda09e..0f291d9f2 100644 --- a/elixir/apps/domain/lib/domain/actors/membership/query.ex +++ b/elixir/apps/domain/lib/domain/actors/membership/query.ex @@ -1,19 +1,82 @@ defmodule Domain.Actors.Membership.Query do use Domain, :query + alias Domain.Actors.{Actor, Group, Membership} def all do - from(memberships in Domain.Actors.Membership, as: :memberships) + from(memberships in Membership, as: :memberships) end def by_actor_id(queryable \\ all(), actor_id) do where(queryable, [memberships: memberships], memberships.actor_id == ^actor_id) end - def by_group_id(queryable \\ all(), group_id) do + def by_group_id(queryable \\ all(), group_id) + + def by_group_id(queryable, {:in, group_ids}) do + where(queryable, [memberships: memberships], memberships.group_id in ^group_ids) + end + + def by_group_id(queryable, group_id) do where(queryable, [memberships: memberships], memberships.group_id == ^group_id) end + def by_group_id_and_actor_id(queryable \\ all(), {:in, tuples}) do + queryable = where(queryable, [], false) + + Enum.reduce(tuples, queryable, fn {group_id, actor_id}, queryable -> + or_where( + queryable, + [memberships: memberships], + memberships.group_id == ^group_id and memberships.actor_id == ^actor_id + ) + end) + end + def by_account_id(queryable \\ all(), account_id) do where(queryable, [memberships: memberships], memberships.account_id == ^account_id) end + + def by_group_provider_id(queryable \\ all(), provider_id) do + queryable + |> with_joined_groups() + |> where([groups: groups], groups.provider_id == ^provider_id) + end + + def count_actors_by_group_id(queryable \\ all()) do + queryable + |> group_by([memberships: memberships], memberships.group_id) + |> with_joined_actors() + |> select([memberships: memberships, actors: actors], %{ + group_id: memberships.group_id, + count: count(actors.id) + }) + end + + def count_groups_by_actor_id(queryable \\ all()) do + queryable + |> group_by([memberships: memberships], memberships.actor_id) + |> with_joined_groups() + |> select([memberships: memberships, groups: groups], %{ + actor_id: memberships.actor_id, + count: count(groups.id) + }) + end + + def with_joined_actors(queryable \\ all()) do + join(queryable, :inner, [memberships: memberships], actors in ^Actor.Query.all(), + on: actors.id == memberships.actor_id, + as: :actors + ) + end + + def with_joined_groups(queryable \\ all()) do + join(queryable, :inner, [memberships: memberships], groups in ^Group.Query.all(), + on: groups.id == memberships.group_id, + as: :groups + ) + end + + def lock(queryable \\ all()) do + lock(queryable, "FOR UPDATE") + end end diff --git a/elixir/apps/domain/lib/domain/actors/membership/sync.ex b/elixir/apps/domain/lib/domain/actors/membership/sync.ex new file mode 100644 index 000000000..3baa1782f --- /dev/null +++ b/elixir/apps/domain/lib/domain/actors/membership/sync.ex @@ -0,0 +1,113 @@ +defmodule Domain.Actors.Membership.Sync do + alias Domain.Auth + alias Domain.Actors.Membership + + def sync_provider_memberships_multi(multi, %Auth.Provider{} = provider, tuples) do + multi + |> Ecto.Multi.all(:memberships, fn _effects_so_far -> + fetch_and_lock_provider_memberships_query(provider) + end) + |> Ecto.Multi.run( + :plan_memberships, + fn _repo, + %{ + identities: identities, + insert_identities: insert_identities, + groups: groups, + upsert_groups: upsert_groups, + memberships: memberships + } -> + plan_memberships_update( + tuples, + identities, + insert_identities, + groups, + upsert_groups, + memberships + ) + end + ) + |> Ecto.Multi.delete_all(:delete_memberships, fn %{plan_memberships: {_upsert, delete}} -> + delete_memberships_query(delete) + end) + |> Ecto.Multi.run(:upsert_memberships, fn repo, %{plan_memberships: {upsert, _delete}} -> + upsert_memberships(repo, provider, upsert) + end) + end + + defp fetch_and_lock_provider_memberships_query(provider) do + Membership.Query.by_account_id(provider.account_id) + |> Membership.Query.by_group_provider_id(provider.id) + |> Membership.Query.lock() + end + + defp plan_memberships_update( + tuples, + identities, + insert_identities, + groups, + upsert_groups, + memberships + ) do + identity_by_provider_identifier = + for identity <- identities ++ insert_identities, into: %{} do + {identity.provider_identifier, identity} + end + + group_by_provider_identifier = + for group <- groups ++ upsert_groups, into: %{} do + {group.provider_identifier, group} + end + + tuples = + Enum.map(tuples, fn {group_provider_identifier, actor_provider_identifier} -> + {Map.fetch!(group_by_provider_identifier, group_provider_identifier).id, + Map.fetch!(identity_by_provider_identifier, actor_provider_identifier).actor_id} + end) + + {upsert, delete} = + Enum.reduce( + memberships, + {tuples, []}, + fn membership, {upsert, delete} -> + tuple = {membership.group_id, membership.actor_id} + + if tuple in tuples do + {upsert -- [tuple], delete} + else + {upsert -- [tuple], [{membership.group_id, membership.actor_id}] ++ delete} + end + end + ) + + {:ok, {upsert, delete}} + end + + defp delete_memberships_query(provider_identifiers_to_delete) do + Membership.Query.by_group_id_and_actor_id({:in, provider_identifiers_to_delete}) + end + + defp upsert_memberships(repo, provider, provider_identifiers_to_upsert) do + provider_identifiers_to_upsert + |> Enum.reduce_while({:ok, []}, fn {group_id, actor_id}, {:ok, acc} -> + attrs = %{group_id: group_id, actor_id: actor_id} + + case upsert_membership(repo, provider, attrs) do + {:ok, membership} -> + {:cont, {:ok, [membership | acc]}} + + {:error, changeset} -> + {:halt, {:error, changeset}} + end + end) + end + + defp upsert_membership(repo, provider, attrs) do + Membership.Changeset.changeset(provider.account_id, %Membership{}, attrs) + |> repo.insert( + conflict_target: Membership.Changeset.upsert_conflict_target(), + on_conflict: Membership.Changeset.upsert_on_conflict(), + returning: true + ) + end +end diff --git a/elixir/apps/domain/lib/domain/auth.ex b/elixir/apps/domain/lib/domain/auth.ex index 815967804..f63e2c04e 100644 --- a/elixir/apps/domain/lib/domain/auth.ex +++ b/elixir/apps/domain/lib/domain/auth.ex @@ -10,6 +10,8 @@ defmodule Domain.Auth do account_user: 24 * 7 } + @max_session_duration_hours @default_session_duration_hours + def start_link(opts) do Supervisor.start_link(__MODULE__, opts, name: __MODULE__) end @@ -49,6 +51,28 @@ defmodule Domain.Auth do end end + @doc """ + This functions allows to fetch singleton providers like `email` or `token`. + """ + def fetch_active_provider_by_adapter(adapter, %Subject{} = subject, opts \\ []) + when adapter in [:email, :token, :userpass] do + with :ok <- ensure_has_permissions(subject, Authorizer.manage_providers_permission()) do + {preload, _opts} = Keyword.pop(opts, :preload, []) + + Provider.Query.by_adapter(adapter) + |> Provider.Query.not_disabled() + |> Authorizer.for_subject(Provider, subject) + |> Repo.fetch() + |> case do + {:ok, provider} -> + {:ok, Repo.preload(provider, preload)} + + {:error, reason} -> + {:error, reason} + end + end + end + def fetch_provider_by_id(id) do if Validator.valid_uuid?(id) do Provider.Query.by_id(id) @@ -104,8 +128,28 @@ defmodule Domain.Auth do |> Repo.list() end + def list_providers_pending_token_refresh_by_adapter(adapter) do + datetime_filter = DateTime.utc_now() |> DateTime.add(1, :hour) + + Provider.Query.by_adapter(adapter) + |> Provider.Query.by_provisioner(:custom) + |> Provider.Query.token_expires_at({:lt, datetime_filter}) + |> Provider.Query.not_disabled() + |> Repo.list() + end + + def list_providers_pending_sync_by_adapter(adapter) do + datetime_filter = DateTime.utc_now() |> DateTime.add(-10, :minute) + + Provider.Query.by_adapter(adapter) + |> Provider.Query.by_provisioner(:custom) + |> Provider.Query.last_synced_at({:lt, datetime_filter}) + |> Provider.Query.not_disabled() + |> Repo.list() + end + def new_provider(%Accounts.Account{} = account, attrs \\ %{}) do - Provider.Changeset.create_changeset(account, attrs) + Provider.Changeset.create(account, attrs) |> Adapters.provider_changeset() end @@ -113,7 +157,7 @@ defmodule Domain.Auth do with :ok <- ensure_has_permissions(subject, Authorizer.manage_providers_permission()), :ok <- Accounts.ensure_has_access_to(subject, account), changeset = - Provider.Changeset.create_changeset(account, attrs, subject) + Provider.Changeset.create(account, attrs, subject) |> Adapters.provider_changeset(), {:ok, provider} <- Repo.insert(changeset) do Adapters.ensure_provisioned(provider) @@ -122,7 +166,7 @@ defmodule Domain.Auth do def create_provider(%Accounts.Account{} = account, attrs) do changeset = - Provider.Changeset.create_changeset(account, attrs) + Provider.Changeset.create(account, attrs) |> Adapters.provider_changeset() with {:ok, provider} <- Repo.insert(changeset) do @@ -131,7 +175,7 @@ defmodule Domain.Auth do end def change_provider(%Provider{} = provider, attrs \\ %{}) do - Provider.Changeset.update_changeset(provider, attrs) + Provider.Changeset.update(provider, attrs) |> Adapters.provider_changeset() end @@ -141,7 +185,7 @@ defmodule Domain.Auth do |> Authorizer.for_subject(Provider, subject) |> Repo.fetch_and_update( with: fn provider -> - Provider.Changeset.update_changeset(provider, attrs) + Provider.Changeset.update(provider, attrs) |> Adapters.provider_changeset() end ) @@ -198,6 +242,7 @@ defmodule Domain.Auth do defp other_active_providers_exist?(%Provider{id: id, account_id: account_id}) do Provider.Query.by_id({:not, id}) + |> Provider.Query.by_adapter({:not_in, [:token]}) |> Provider.Query.not_disabled() |> Provider.Query.by_account_id(account_id) |> Provider.Query.lock() @@ -210,6 +255,20 @@ defmodule Domain.Auth do # Identities + def fetch_identity_by_id(id, %Subject{} = subject) do + with :ok <- ensure_has_permissions(subject, Authorizer.manage_identities_permission()) do + Identity.Query.by_id(id) + |> Authorizer.for_subject(Identity, subject) + |> Repo.fetch() + end + end + + def fetch_active_identity_by_id(id) do + Identity.Query.by_id(id) + |> Identity.Query.not_disabled() + |> Repo.fetch() + end + def fetch_identity_by_id(id) do Identity.Query.by_id(id) |> Repo.fetch() @@ -236,14 +295,13 @@ defmodule Domain.Auth do end end - def upsert_identity( - %Actors.Actor{} = actor, - %Provider{} = provider, - provider_identifier, - provider_attrs \\ %{} - ) do - Identity.Changeset.create_identity(actor, provider, provider_identifier) - |> Adapters.identity_changeset(provider, provider_attrs) + def sync_provider_identities_multi(%Provider{} = provider, attrs_list) do + Identity.Sync.sync_provider_identities_multi(provider, attrs_list) + end + + def upsert_identity(%Actors.Actor{} = actor, %Provider{} = provider, attrs) do + Identity.Changeset.create_identity(actor, provider, attrs) + |> Adapters.identity_changeset(provider) |> Repo.insert( conflict_target: {:unsafe_fragment, @@ -260,23 +318,29 @@ defmodule Domain.Auth do ) end + def new_identity(%Actors.Actor{} = actor, %Provider{} = provider, attrs \\ %{}) do + Identity.Changeset.create_identity(actor, provider, attrs) + |> Adapters.identity_changeset(provider) + end + def create_identity( %Actors.Actor{} = actor, %Provider{} = provider, - provider_identifier, - provider_attrs \\ %{} + attrs, + %Subject{} = subject ) do - Identity.Changeset.create_identity(actor, provider, provider_identifier) - |> Adapters.identity_changeset(provider, provider_attrs) + with :ok <- ensure_has_permissions(subject, Authorizer.manage_identities_permission()) do + create_identity(actor, provider, attrs) + end + end + + def create_identity(%Actors.Actor{} = actor, %Provider{} = provider, attrs) do + Identity.Changeset.create_identity(actor, provider, attrs) + |> Adapters.identity_changeset(provider) |> Repo.insert() end - def replace_identity( - %Identity{} = identity, - provider_identifier, - provider_attrs \\ %{}, - %Subject{} = subject - ) do + def replace_identity(%Identity{} = identity, attrs, %Subject{} = subject) do required_permissions = {:one_of, [ @@ -294,13 +358,8 @@ defmodule Domain.Auth do |> Repo.fetch() end) |> Ecto.Multi.insert(:new_identity, fn %{identity: identity} -> - Identity.Changeset.create_identity( - identity.actor, - identity.provider, - provider_identifier, - subject - ) - |> Adapters.identity_changeset(identity.provider, provider_attrs) + Identity.Changeset.create_identity(identity.actor, identity.provider, attrs, subject) + |> Adapters.identity_changeset(identity.provider) end) |> Ecto.Multi.update(:deleted_identity, fn %{identity: identity} -> Identity.Changeset.delete_identity(identity) @@ -316,6 +375,10 @@ defmodule Domain.Auth do end end + def delete_identity(%Identity{created_by: :provider}, %Subject{}) do + {:error, :cant_delete_synced_identity} + end + def delete_identity(%Identity{} = identity, %Subject{} = subject) do required_permissions = {:one_of, @@ -331,11 +394,26 @@ defmodule Domain.Auth do end end + def delete_actor_identities(%Actors.Actor{} = actor) do + {_count, nil} = + Identity.Query.by_actor_id(actor.id) + |> Repo.update_all(set: [deleted_at: DateTime.utc_now(), provider_state: %{}]) + + :ok + end + + def identity_disabled?(%{disabled_at: nil}), do: false + def identity_disabled?(_identity), do: true + + def identity_deleted?(%{deleted_at: nil}), do: false + def identity_deleted?(_identity), do: true + # Sign Up / In / Off def sign_in(%Provider{} = provider, id_or_provider_identifier, secret, user_agent, remote_ip) do identity_queryable = - Identity.Query.by_provider_id(provider.id) + Identity.Query.not_disabled() + |> Identity.Query.by_provider_id(provider.id) |> Identity.Query.by_id_or_provider_identifier(id_or_provider_identifier) with {:ok, identity} <- Repo.fetch(identity_queryable), @@ -359,8 +437,7 @@ defmodule Domain.Auth do end def sign_in(token, user_agent, remote_ip) when is_binary(token) do - with {:ok, identity, expires_at} <- - verify_token(token, user_agent, remote_ip) do + with {:ok, identity, expires_at} <- verify_token(token, user_agent, remote_ip) do {:ok, build_subject(identity, expires_at, user_agent, remote_ip)} else {:error, :not_found} -> {:error, :unauthorized} @@ -398,8 +475,15 @@ defmodule Domain.Auth do end defp build_subject_expires_at(%Actors.Actor{} = actor, expires_at) do + now = DateTime.utc_now() + default_session_duration_hours = Map.fetch!(@default_session_duration_hours, actor.type) - expires_at || DateTime.utc_now() |> DateTime.add(default_session_duration_hours, :hour) + expires_at = expires_at || DateTime.add(now, default_session_duration_hours, :hour) + + max_session_duration_hours = Map.fetch!(@max_session_duration_hours, actor.type) + max_expires_at = DateTime.add(now, max_session_duration_hours, :hour) + + Enum.min([expires_at, max_expires_at], DateTime) end # Session @@ -482,7 +566,7 @@ defmodule Domain.Auth do _user_agent, _remote_ip ) do - with {:ok, identity} <- fetch_identity_by_id(identity_id), + with {:ok, identity} <- fetch_active_identity_by_id(identity_id), {:ok, provider} <- fetch_active_provider_by_id(identity.provider_id), {:ok, identity, expires_at} <- Adapters.verify_secret(provider, identity, secret) do @@ -500,7 +584,7 @@ defmodule Domain.Auth do _user_agent, _remote_ip ) do - with {:ok, identity} <- fetch_identity_by_id(identity_id), + with {:ok, identity} <- fetch_active_identity_by_id(identity_id), {:ok, expires_at} <- fetch_session_token_expires_at(token) do {:ok, identity, expires_at} end @@ -512,7 +596,7 @@ defmodule Domain.Auth do user_agent, remote_ip ) do - with {:ok, identity} <- fetch_identity_by_id(identity_id), + with {:ok, identity} <- fetch_active_identity_by_id(identity_id), true <- context_payload == session_context_payload(remote_ip, user_agent), {:ok, expires_at} <- fetch_session_token_expires_at(token) do {:ok, identity, expires_at} @@ -531,7 +615,7 @@ defmodule Domain.Auth do end defp access_token_payload(%Identity{} = identity) do - {:identity, identity.id, identity.provider_virtual_state.secret, :ignore} + {:identity, identity.id, identity.provider_virtual_state.changes.secret, :ignore} end defp fetch_config! do @@ -588,4 +672,9 @@ defmodule Domain.Auth do missing_permissions -> {:error, {:unauthorized, missing_permissions: missing_permissions}} end end + + def can_grant_role?(%Subject{} = subject, granted_role) do + granted_permissions = fetch_type_permissions!(granted_role) + MapSet.subset?(granted_permissions, subject.permissions) + end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters.ex b/elixir/apps/domain/lib/domain/auth/adapters.ex index 4d69b2546..a940c4899 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters.ex @@ -36,9 +36,8 @@ defmodule Domain.Auth.Adapters do fetch_adapter!(adapter).capabilities() end - def identity_changeset(%Ecto.Changeset{} = changeset, %Provider{} = provider, provider_attrs) do + def identity_changeset(%Ecto.Changeset{} = changeset, %Provider{} = provider) do adapter = fetch_provider_adapter!(provider) - changeset = Ecto.Changeset.put_change(changeset, :provider_virtual_state, provider_attrs) %Ecto.Changeset{} = adapter.identity_changeset(provider, changeset) end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace.ex index 97aab0a78..0accf3868 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace.ex @@ -72,4 +72,8 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace do def verify_and_upsert_identity(%Actors.Actor{} = actor, %Provider{} = provider, payload) do OpenIDConnect.verify_and_upsert_identity(actor, provider, payload) end + + def refresh_access_token(%Provider{} = provider) do + OpenIDConnect.refresh_access_token(provider) + end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs.ex index b7118c877..02285b73c 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs.ex @@ -1,9 +1,101 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs do use Domain.Jobs.Recurrent, otp_app: :domain + alias Domain.{Auth, Actors} + alias Domain.Auth.Adapters.GoogleWorkspace require Logger every minutes(5), :refresh_access_tokens do - Logger.debug("Refreshing tokens") - :ok + with {:ok, providers} <- + Domain.Auth.list_providers_pending_token_refresh_by_adapter(:google_workspace) do + Enum.each(providers, fn provider -> + Logger.debug("Refreshing tokens for #{inspect(provider)}") + GoogleWorkspace.refresh_access_token(provider) + end) + end + end + + every minutes(3), :sync_directory do + with {:ok, providers} <- Domain.Auth.list_providers_pending_sync_by_adapter(:google_workspace) do + Logger.debug("Syncing #{length(providers)} providers") + + providers + |> Enum.chunk_every(5) + |> Enum.each(fn providers -> + Enum.map(providers, fn provider -> + Logger.debug("Syncing provider", provider_id: provider.id) + + access_token = provider.adapter_state[:access_token] + + with {:ok, users} <- GoogleWorkspace.APIClient.list_users(access_token), + {:ok, organization_units} <- + GoogleWorkspace.APIClient.list_organization_units(access_token), + {:ok, groups} <- GoogleWorkspace.APIClient.list_groups(access_token), + {:ok, tuples} <- + list_membership_tuples(access_token, groups) do + identities_attrs = + Enum.map(users, fn user -> + %{ + "provider_identifier" => user["id"], + "actor" => %{ + "type" => :account_user, + "name" => user["name"]["fullName"] + } + } + end) + + actor_groups_attrs = + Enum.map(groups, fn group -> + %{ + "name" => "Group:" <> group["name"], + "provider_identifier" => "G:" <> group["id"] + } + end) ++ + Enum.map(organization_units, fn organization_unit -> + %{ + "name" => "OrgUnit:" <> organization_unit["name"], + "provider_identifier" => "OU:" <> organization_unit["orgUnitId"] + } + end) + + tuples = + Enum.flat_map(users, fn user -> + organization_unit = + Enum.find(organization_units, fn organization_unit -> + organization_unit["orgUnitPath"] == user["orgUnitPath"] + end) + + [{"OU:" <> organization_unit["orgUnitId"], user["id"]}] + end) ++ tuples + + Ecto.Multi.new() + |> Ecto.Multi.append(Auth.sync_provider_identities_multi(provider, identities_attrs)) + |> Ecto.Multi.append(Actors.sync_provider_groups_multi(provider, actor_groups_attrs)) + |> Actors.sync_provider_memberships_multi(provider, tuples) + |> Domain.Repo.transaction() + + Logger.debug("Finished syncing provider", provider_id: provider.id) + else + {:error, reason} -> + Logger.error("Failed syncing provider", + provider_id: provider.id, + reason: inspect(reason) + ) + end + end) + end) + end + end + + defp list_membership_tuples(access_token, groups) do + Enum.reduce_while(groups, {:ok, []}, fn group, {:ok, tuples} -> + case GoogleWorkspace.APIClient.list_group_members(access_token, group["id"]) do + {:ok, members} -> + tuples = Enum.map(members, &{"G:" <> group["id"], &1["id"]}) ++ tuples + {:cont, {:ok, tuples}} + + {:error, reason} -> + {:halt, {:error, reason}} + end + end) end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex index ad9f5105b..35b27ee37 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex @@ -87,16 +87,28 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do @impl true def verify_and_update_identity(%Provider{} = provider, {redirect_uri, code_verifier, code}) do - provider - |> sync_identity(%{ + token_params = %{ grant_type: "authorization_code", redirect_uri: redirect_uri, code: code, code_verifier: code_verifier - }) - |> case do - {:ok, identity, expires_at} -> {:ok, identity, expires_at} - {:error, :not_found} -> {:error, :not_found} + } + + with {:ok, provider_identifier, identity_state} <- + fetch_state(provider, token_params) do + Identity.Query.not_disabled() + |> Identity.Query.by_provider_id(provider.id) + |> Identity.Query.by_provider_identifier(provider_identifier) + |> Repo.fetch_and_update( + with: fn identity -> + Identity.Changeset.update_identity_provider_state(identity, identity_state) + end + ) + |> case do + {:ok, identity} -> {:ok, identity, identity_state.expires_at} + {:error, reason} -> {:error, reason} + end + else {:error, :expired_token} -> {:error, :expired} {:error, :invalid_token} -> {:error, :invalid} {:error, :internal_error} -> {:error, :internal_error} @@ -116,31 +128,41 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do } with {:ok, provider_identifier, identity_state} <- - fetch_identity_state(provider, token_params) do - Domain.Auth.upsert_identity(actor, provider, provider_identifier, identity_state) + fetch_state(provider, token_params) do + Domain.Auth.upsert_identity(actor, provider, %{ + provider_identifier: provider_identifier, + provider_virtual_state: identity_state + }) end end - def refresh_token(%Identity{} = identity) do - identity = Repo.preload(identity, :provider) - - sync_identity(identity.provider, %{ + def refresh_access_token(%Provider{} = provider) do + token_params = %{ grant_type: "refresh_token", - refresh_token: identity.provider_state["refresh_token"] - }) + refresh_token: provider.adapter_state["refresh_token"] + } + + with {:ok, _provider_identifier, adapter_state} <- + fetch_state(provider, token_params) do + Provider.Query.by_id(provider.id) + |> Repo.fetch_and_update( + with: fn provider -> + Provider.Changeset.update(provider, %{adapter_state: adapter_state}) + end + ) + else + {:error, :expired_token} -> {:error, :expired} + {:error, :invalid_token} -> {:error, :invalid} + {:error, :internal_error} -> {:error, :internal_error} + end end - defp fetch_identity_state(%Provider{} = provider, token_params) do + defp fetch_state(%Provider{} = provider, token_params) do config = config_for_provider(provider) with {:ok, tokens} <- OpenIDConnect.fetch_tokens(config, token_params), {:ok, claims} <- OpenIDConnect.verify(config, tokens["id_token"]), {:ok, userinfo} <- OpenIDConnect.fetch_userinfo(config, tokens["access_token"]) do - # TODO: sync groups - # TODO: refresh the access token so it doesn't expire - # TODO: first admin user token that configured provider should used for periodic syncs - # TODO: active status for relays, gateways in list functions - # TODO: JIT provisioning expires_at = cond do not is_nil(tokens["expires_in"]) -> @@ -180,23 +202,6 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do end end - defp sync_identity(%Provider{} = provider, token_params) do - with {:ok, provider_identifier, identity_state} <- - fetch_identity_state(provider, token_params) do - Identity.Query.by_provider_id(provider.id) - |> Identity.Query.by_provider_identifier(provider_identifier) - |> Repo.fetch_and_update( - with: fn identity -> - Identity.Changeset.update_identity_provider_state(identity, identity_state) - end - ) - |> case do - {:ok, identity} -> {:ok, identity, identity_state.expires_at} - {:error, reason} -> {:error, reason} - end - end - end - defp config_for_provider(%Provider{} = provider) do Ecto.embedded_load(Settings, provider.adapter_config, :json) |> Map.from_struct() diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/settings/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/settings/changeset.ex index bbcb6c41b..c2281e077 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/settings/changeset.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect/settings/changeset.ex @@ -1,16 +1,11 @@ defmodule Domain.Auth.Adapters.OpenIDConnect.Settings.Changeset do use Domain, :changeset - alias Domain.Auth.Adapters.OpenIDConnect.Settings @fields ~w[scope response_type client_id client_secret discovery_document_uri]a - def create_changeset(attrs) do - changeset(%Settings{}, attrs) - end - def changeset(struct, attrs) do struct |> cast(attrs, @fields) diff --git a/elixir/apps/domain/lib/domain/auth/adapters/token.ex b/elixir/apps/domain/lib/domain/auth/adapters/token.ex index 12c7e361e..3502c3bd8 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/token.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/token.ex @@ -60,15 +60,17 @@ defmodule Domain.Auth.Adapters.Token do %{valid?: true} = nested_changeset -> expires_at = Ecto.Changeset.fetch_change!(nested_changeset, :expires_at) + nested_changeset = Ecto.Changeset.put_change(nested_changeset, :secret, secret) + + {changeset, _original_type} = + changeset + |> Ecto.Changeset.put_change(:provider_state, %{ + "expires_at" => DateTime.to_iso8601(expires_at), + "secret_hash" => secret_hash + }) + |> Domain.Changeset.inject_embedded_changeset(:provider_virtual_state, nested_changeset) changeset - |> Ecto.Changeset.put_change(:provider_state, %{ - "expires_at" => DateTime.to_iso8601(expires_at), - "secret_hash" => secret_hash - }) - |> Ecto.Changeset.put_change(:provider_virtual_state, %{ - secret: secret - }) end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/token/state.ex b/elixir/apps/domain/lib/domain/auth/adapters/token/state.ex index c0e1e3aec..68fe7bd45 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/token/state.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/token/state.ex @@ -3,6 +3,7 @@ defmodule Domain.Auth.Adapters.Token.State do @primary_key false embedded_schema do + field :secret, :string, virtual: true, redact: true field :secret_hash, :string, redact: true field :expires_at, :utc_datetime_usec end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/token/state/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/token/state/changeset.ex index a8f659563..baa2d9185 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/token/state/changeset.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/token/state/changeset.ex @@ -1,13 +1,8 @@ defmodule Domain.Auth.Adapters.Token.State.Changeset do use Domain, :changeset - alias Domain.Auth.Adapters.Token.State @fields ~w[expires_at]a - def create_changeset(attrs) do - changeset(%State{}, attrs) - end - def changeset(struct, attrs) do struct |> cast(attrs, @fields) diff --git a/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex b/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex index 9ef9f46a8..cc9ca29e2 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex @@ -56,11 +56,19 @@ defmodule Domain.Auth.Adapters.UserPass do changeset %{valid?: true} = nested_changeset -> + nested_changeset = + nested_changeset + |> Domain.Validator.redact_field(:password) + |> Domain.Validator.redact_field(:password_confirmation) + password_hash = Ecto.Changeset.fetch_change!(nested_changeset, :password_hash) + {changeset, _original_type} = + changeset + |> Ecto.Changeset.put_change(:provider_state, %{"password_hash" => password_hash}) + |> Domain.Changeset.inject_embedded_changeset(:provider_virtual_state, nested_changeset) + changeset - |> Ecto.Changeset.put_change(:provider_state, %{"password_hash" => password_hash}) - |> Ecto.Changeset.put_change(:provider_virtual_state, %{}) end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/userpass/password/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/userpass/password/changeset.ex index f7123895e..39485df97 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/userpass/password/changeset.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/userpass/password/changeset.ex @@ -2,15 +2,11 @@ defmodule Domain.Auth.Adapters.UserPass.Password.Changeset do use Domain, :changeset alias Domain.Auth.Adapters.UserPass.Password - @fields ~w[password]a + @fields ~w[password password_confirmation]a @min_password_length 12 @max_password_length 72 - def create_changeset(attrs) do - changeset(%Password{}, attrs) - end - - def changeset(struct, attrs) do + def changeset(%Password{} = struct, attrs) do struct |> cast(attrs, @fields) |> validate_required(@fields) @@ -28,8 +24,6 @@ defmodule Domain.Auth.Adapters.UserPass.Password.Changeset do # |> validate_no_sequential_characters(:password) # |> validate_no_public_context(:password) |> put_hash(:password, to: :password_hash) - |> redact_field(:password) - |> redact_field(:password_confirmation) |> validate_required([:password_hash]) end end diff --git a/elixir/apps/domain/lib/domain/auth/authorizer.ex b/elixir/apps/domain/lib/domain/auth/authorizer.ex index 99a03fd0a..79e1547cd 100644 --- a/elixir/apps/domain/lib/domain/auth/authorizer.ex +++ b/elixir/apps/domain/lib/domain/auth/authorizer.ex @@ -40,6 +40,7 @@ defmodule Domain.Auth.Authorizer do def list_permissions_for_role(:account_admin_user) do [ manage_providers_permission(), + manage_own_identities_permission(), manage_identities_permission() ] end diff --git a/elixir/apps/domain/lib/domain/auth/identity.ex b/elixir/apps/domain/lib/domain/auth/identity.ex index 82b75b877..1647a9db6 100644 --- a/elixir/apps/domain/lib/domain/auth/identity.ex +++ b/elixir/apps/domain/lib/domain/auth/identity.ex @@ -18,6 +18,9 @@ defmodule Domain.Auth.Identity do field :created_by, Ecto.Enum, values: ~w[system provider identity]a belongs_to :created_by_identity, Domain.Auth.Identity + has_many :devices, Domain.Devices.Device, where: [deleted_at: nil] + field :deleted_at, :utc_datetime_usec + timestamps(updated_at: false) end end diff --git a/elixir/apps/domain/lib/domain/auth/identity/changeset.ex b/elixir/apps/domain/lib/domain/auth/identity/changeset.ex index f3d79dfa5..aef09e6a7 100644 --- a/elixir/apps/domain/lib/domain/auth/identity/changeset.ex +++ b/elixir/apps/domain/lib/domain/auth/identity/changeset.ex @@ -3,9 +3,14 @@ defmodule Domain.Auth.Identity.Changeset do alias Domain.Actors alias Domain.Auth.{Subject, Identity, Provider} - def create_identity(actor, provider, provider_identifier, %Subject{} = subject) do + def create_identity( + %Actors.Actor{} = actor, + %Provider{} = provider, + attrs, + %Subject{} = subject + ) do actor - |> create_identity(provider, provider_identifier) + |> create_identity(provider, attrs) |> put_change(:created_by, :identity) |> put_change(:created_by_identity_id, subject.identity.id) end @@ -13,19 +18,42 @@ defmodule Domain.Auth.Identity.Changeset do def create_identity( %Actors.Actor{account_id: account_id} = actor, %Provider{account_id: account_id} = provider, - provider_identifier + attrs ) do %Identity{} - |> change() + |> cast(attrs, ~w[provider_identifier provider_virtual_state]a) + |> validate_required(~w[provider_identifier]a) |> put_change(:actor_id, actor.id) |> put_change(:provider_id, provider.id) |> put_change(:account_id, account_id) - |> put_change(:provider_identifier, provider_identifier) + |> put_change(:created_by, :system) + |> changeset() + end + + def create_identity_and_actor( + %Provider{account_id: account_id} = provider, + attrs + ) do + %Identity{} + |> cast(attrs, ~w[provider_identifier provider_virtual_state]a) + |> validate_required(~w[provider_identifier]a) + |> cast_assoc(:actor, + with: fn _actor, attrs -> + Actors.Actor.Changeset.create(account_id, attrs) + |> put_change(:last_synced_at, DateTime.utc_now()) + end + ) + |> put_change(:provider_id, provider.id) + |> put_change(:account_id, account_id) + |> put_change(:created_by, :provider) + |> changeset() + end + + def changeset(changeset) do + changeset |> unique_constraint(:provider_identifier, name: :auth_identities_account_id_provider_id_provider_identifier_idx ) - |> validate_required(:provider_identifier) - |> put_change(:created_by, :system) end def update_identity_provider_state(identity_or_changeset, %{} = state, virtual_state \\ %{}) do diff --git a/elixir/apps/domain/lib/domain/auth/identity/query.ex b/elixir/apps/domain/lib/domain/auth/identity/query.ex index 3150cf6af..36b91ccaa 100644 --- a/elixir/apps/domain/lib/domain/auth/identity/query.ex +++ b/elixir/apps/domain/lib/domain/auth/identity/query.ex @@ -4,9 +4,6 @@ defmodule Domain.Auth.Identity.Query do def all do from(identities in Domain.Auth.Identity, as: :identities) |> where([identities: identities], is_nil(identities.deleted_at)) - |> join(:inner, [identities: identities], actors in assoc(identities, :actor), as: :actors) - |> where([actors: actors], is_nil(actors.deleted_at)) - |> where([actors: actors], is_nil(actors.disabled_at)) end def by_id(queryable \\ all(), id) @@ -38,7 +35,17 @@ defmodule Domain.Auth.Identity.Query do where(queryable, [identities: identities], identities.adapter == ^adapter) end - def by_provider_identifier(queryable \\ all(), provider_identifier) do + def by_provider_identifier(queryable \\ all(), provider_identifier) + + def by_provider_identifier(queryable, {:in, provider_identifiers}) do + where( + queryable, + [identities: identities], + identities.provider_identifier in ^provider_identifiers + ) + end + + def by_provider_identifier(queryable, provider_identifier) do where( queryable, [identities: identities], @@ -59,6 +66,13 @@ defmodule Domain.Auth.Identity.Query do end end + def not_disabled(queryable \\ all()) do + queryable + |> join(:inner, [identities: identities], actors in assoc(identities, :actor), as: :actors) + |> where([actors: actors], is_nil(actors.deleted_at)) + |> where([actors: actors], is_nil(actors.disabled_at)) + end + def lock(queryable \\ all()) do lock(queryable, "FOR UPDATE") end diff --git a/elixir/apps/domain/lib/domain/auth/identity/sync.ex b/elixir/apps/domain/lib/domain/auth/identity/sync.ex new file mode 100644 index 000000000..91a12c719 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/identity/sync.ex @@ -0,0 +1,78 @@ +defmodule Domain.Auth.Identity.Sync do + alias Domain.Auth.{Identity, Provider} + + def sync_provider_identities_multi(%Provider{} = provider, attrs_list) do + now = DateTime.utc_now() + + attrs_by_provider_identifier = + for attrs <- attrs_list, into: %{} do + {Map.fetch!(attrs, "provider_identifier"), attrs} + end + + provider_identifiers = Map.keys(attrs_by_provider_identifier) + + Ecto.Multi.new() + |> Ecto.Multi.all(:identities, fn _effects_so_far -> + fetch_and_lock_provider_identities_query(provider) + end) + |> Ecto.Multi.run(:plan_identities, fn _repo, %{identities: identities} -> + plan_identities_update(identities, provider_identifiers) + end) + |> Ecto.Multi.update_all( + :delete_identities, + fn %{plan_identities: {_insert, delete}} -> + delete_identities_query(provider, delete) + end, + set: [deleted_at: now] + ) + |> Ecto.Multi.run(:insert_identities, fn repo, %{plan_identities: {insert, _delete}} -> + upsert_identities(repo, provider, attrs_by_provider_identifier, insert) + end) + end + + defp fetch_and_lock_provider_identities_query(provider) do + Identity.Query.by_account_id(provider.account_id) + |> Identity.Query.by_provider_id(provider.id) + |> Identity.Query.lock() + end + + defp plan_identities_update(identities, provider_identifiers) do + {insert, delete} = + Enum.reduce(identities, {provider_identifiers, []}, fn identity, {insert, delete} -> + if identity.provider_identifier in provider_identifiers do + {insert -- [identity.provider_identifier], delete} + else + {insert -- [identity.provider_identifier], [identity.provider_identifier] ++ delete} + end + end) + + {:ok, {insert, delete}} + end + + defp delete_identities_query(provider, provider_identifiers_to_delete) do + Identity.Query.by_account_id(provider.account_id) + |> Identity.Query.by_provider_id(provider.id) + |> Identity.Query.by_provider_identifier({:in, provider_identifiers_to_delete}) + end + + defp upsert_identities( + repo, + provider, + attrs_by_provider_identifier, + provider_identifiers_to_insert + ) do + provider_identifiers_to_insert + |> Enum.reduce_while({:ok, []}, fn provider_identifier, {:ok, acc} -> + attrs = Map.get(attrs_by_provider_identifier, provider_identifier) + changeset = Identity.Changeset.create_identity_and_actor(provider, attrs) + + case repo.insert(changeset) do + {:ok, identity} -> + {:cont, {:ok, [identity | acc]}} + + {:error, changeset} -> + {:halt, {:error, changeset}} + end + end) + end +end diff --git a/elixir/apps/domain/lib/domain/auth/provider.ex b/elixir/apps/domain/lib/domain/auth/provider.ex index 0bdd5422c..d19452a3c 100644 --- a/elixir/apps/domain/lib/domain/auth/provider.ex +++ b/elixir/apps/domain/lib/domain/auth/provider.ex @@ -11,7 +11,8 @@ defmodule Domain.Auth.Provider do belongs_to :account, Domain.Accounts.Account - has_many :groups, Domain.Actors.Group, where: [deleted_at: nil] + has_many :actor_groups, Domain.Actors.Group, where: [deleted_at: nil] + has_many :identities, Domain.Auth.Identity, where: [deleted_at: nil] field :created_by, Ecto.Enum, values: ~w[system identity]a belongs_to :created_by_identity, Domain.Auth.Identity diff --git a/elixir/apps/domain/lib/domain/auth/provider/changeset.ex b/elixir/apps/domain/lib/domain/auth/provider/changeset.ex index 8516d4afe..80135d7a4 100644 --- a/elixir/apps/domain/lib/domain/auth/provider/changeset.ex +++ b/elixir/apps/domain/lib/domain/auth/provider/changeset.ex @@ -7,14 +7,14 @@ defmodule Domain.Auth.Provider.Changeset do @update_fields ~w[name adapter_config adapter_state provisioner disabled_at deleted_at]a @required_fields ~w[name adapter adapter_config provisioner]a - def create_changeset(account, attrs, %Subject{} = subject) do + def create(account, attrs, %Subject{} = subject) do account - |> create_changeset(attrs) + |> create(attrs) |> put_change(:created_by, :identity) |> put_change(:created_by_identity_id, subject.identity.id) end - def create_changeset(%Accounts.Account{} = account, attrs) do + def create(%Accounts.Account{} = account, attrs) do %Provider{} |> cast(attrs, @create_fields) |> put_change(:account_id, account.id) @@ -22,7 +22,7 @@ defmodule Domain.Auth.Provider.Changeset do |> put_change(:created_by, :system) end - def update_changeset(%Provider{} = provider, attrs) do + def update(%Provider{} = provider, attrs) do provider |> cast(attrs, @update_fields) |> changeset() diff --git a/elixir/apps/domain/lib/domain/auth/provider/query.ex b/elixir/apps/domain/lib/domain/auth/provider/query.ex index e84e76d80..65e73363e 100644 --- a/elixir/apps/domain/lib/domain/auth/provider/query.ex +++ b/elixir/apps/domain/lib/domain/auth/provider/query.ex @@ -16,6 +16,36 @@ defmodule Domain.Auth.Provider.Query do where(queryable, [provider: provider], provider.id == ^id) end + def by_adapter(queryable \\ all(), adapter) + + def by_adapter(queryable, {:not_in, adapters}) do + where(queryable, [provider: provider], provider.adapter not in ^adapters) + end + + def by_adapter(queryable, adapter) do + where(queryable, [provider: provider], provider.adapter == ^adapter) + end + + def last_synced_at(queryable \\ all(), {:lt, datetime}) do + where( + queryable, + [provider: provider], + provider.last_synced_at < ^datetime or is_nil(provider.last_synced_at) + ) + end + + def token_expires_at(queryable \\ all(), {:lt, datetime}) do + where( + queryable, + [provider: provider], + fragment("(?->>'expires_at')::timestamp < ?", provider.adapter_state, ^datetime) + ) + end + + def by_provisioner(queryable \\ all(), provisioner) do + where(queryable, [provider: provider], provider.provisioner == ^provisioner) + end + def by_account_id(queryable \\ all(), account_id) do where(queryable, [provider: provider], provider.account_id == ^account_id) end diff --git a/elixir/apps/domain/lib/domain/devices.ex b/elixir/apps/domain/lib/domain/devices.ex index b66efbcae..4ac1341f1 100644 --- a/elixir/apps/domain/lib/domain/devices.ex +++ b/elixir/apps/domain/lib/domain/devices.ex @@ -27,8 +27,6 @@ defmodule Domain.Devices do end def fetch_device_by_id(id, %Auth.Subject{} = subject, opts \\ []) do - {preload, _opts} = Keyword.pop(opts, :preload, []) - required_permissions = {:one_of, [ @@ -38,12 +36,22 @@ defmodule Domain.Devices do with :ok <- Auth.ensure_has_permissions(subject, required_permissions), true <- Validator.valid_uuid?(id) do + {preload, _opts} = Keyword.pop(opts, :preload, []) + Device.Query.by_id(id) |> Authorizer.for_subject(subject) |> Repo.fetch() |> case do - {:ok, device} -> {:ok, Repo.preload(device, preload)} - {:error, reason} -> {:error, reason} + {:ok, device} -> + device = + device + |> Repo.preload(preload) + |> preload_online_status() + + {:ok, device} + + {:error, reason} -> + {:error, reason} end else false -> {:error, :not_found} @@ -57,6 +65,7 @@ defmodule Domain.Devices do Device.Query.by_id(id) |> Repo.one!() |> Repo.preload(preload) + |> preload_online_status() end def list_devices(%Auth.Subject{} = subject, opts \\ []) do @@ -75,6 +84,10 @@ defmodule Domain.Devices do |> Authorizer.for_subject(subject) |> Repo.list() + devices = + devices + |> preload_online_statuses() + {:ok, Repo.preload(devices, preload)} end end @@ -93,22 +106,45 @@ defmodule Domain.Devices do with :ok <- Auth.ensure_has_permissions(subject, required_permissions), true <- Validator.valid_uuid?(actor_id) do - Device.Query.by_actor_id(actor_id) - |> Authorizer.for_subject(subject) - |> Repo.list() + {:ok, devices} = + Device.Query.by_actor_id(actor_id) + |> Authorizer.for_subject(subject) + |> Repo.list() + + devices = + devices + |> preload_online_statuses() + + {:ok, devices} else false -> {:error, :not_found} other -> other end end + # TODO: this is ugly! + defp preload_online_status(device) do + connected_devices = Presence.list("devices:#{device.id}") + %{device | online?: Map.has_key?(connected_devices, device.id)} + end + + defp preload_online_statuses([]), do: [] + + defp preload_online_statuses([device | _] = devices) do + connected_devices = Presence.list("devices:#{device.account_id}") + + Enum.map(devices, fn device -> + %{device | online?: Map.has_key?(connected_devices, device.id)} + end) + end + def change_device(%Device{} = device, attrs \\ %{}) do - Device.Changeset.update_changeset(device, attrs) + Device.Changeset.update(device, attrs) end def upsert_device(attrs \\ %{}, %Auth.Subject{identity: %Auth.Identity{} = identity} = subject) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_own_devices_permission()) do - changeset = Device.Changeset.upsert_changeset(identity, subject.context, attrs) + changeset = Device.Changeset.upsert(identity, subject.context, attrs) Ecto.Multi.new() |> Ecto.Multi.insert(:device, changeset, @@ -120,7 +156,7 @@ defmodule Domain.Devices do |> resolve_address_multi(:ipv6) |> Ecto.Multi.update(:device_with_address, fn %{device: %Device{} = device, ipv4: ipv4, ipv6: ipv6} -> - Device.Changeset.finalize_upsert_changeset(device, ipv4, ipv6) + Device.Changeset.finalize_upsert(device, ipv4, ipv6) end) |> Repo.transaction() |> case do @@ -144,7 +180,14 @@ defmodule Domain.Devices do with :ok <- authorize_actor_device_management(device.actor_id, subject) do Device.Query.by_id(device.id) |> Authorizer.for_subject(subject) - |> Repo.fetch_and_update(with: &Device.Changeset.update_changeset(&1, attrs)) + |> Repo.fetch_and_update(with: &Device.Changeset.update(&1, attrs)) + |> case do + {:ok, device} -> + {:ok, preload_online_status(device)} + + {:error, reason} -> + {:error, reason} + end end end @@ -152,10 +195,18 @@ defmodule Domain.Devices do with :ok <- authorize_actor_device_management(device.actor_id, subject) do Device.Query.by_id(device.id) |> Authorizer.for_subject(subject) - |> Repo.fetch_and_update(with: &Device.Changeset.delete_changeset/1) + |> Repo.fetch_and_update(with: &Device.Changeset.delete/1) end end + def delete_actor_devices(%Actors.Actor{} = actor) do + {_count, nil} = + Device.Query.by_actor_id(actor.id) + |> Repo.update_all(set: [deleted_at: DateTime.utc_now()]) + + :ok + end + def authorize_actor_device_management(%Actors.Actor{} = actor, %Auth.Subject{} = subject) do authorize_actor_device_management(actor.id, subject) end @@ -169,13 +220,13 @@ defmodule Domain.Devices do end def connect_device(%Device{} = device) do - Phoenix.PubSub.subscribe(Domain.PubSub, "actor:#{device.actor_id}") - {:ok, _} = Presence.track(self(), "devices:#{device.account_id}", device.id, %{ online_at: System.system_time(:second) }) + {:ok, _} = Presence.track(self(), "actor_devices:#{device.actor_id}", device.id, %{}) + :ok end diff --git a/elixir/apps/domain/lib/domain/devices/device.ex b/elixir/apps/domain/lib/domain/devices/device.ex index 428d54f7c..1ce0f5ba8 100644 --- a/elixir/apps/domain/lib/domain/devices/device.ex +++ b/elixir/apps/domain/lib/domain/devices/device.ex @@ -16,6 +16,8 @@ defmodule Domain.Devices.Device do field :last_seen_version, :string field :last_seen_at, :utc_datetime_usec + field :online?, :boolean, virtual: true + belongs_to :account, Domain.Accounts.Account belongs_to :actor, Domain.Actors.Actor belongs_to :identity, Domain.Auth.Identity diff --git a/elixir/apps/domain/lib/domain/devices/device/changeset.ex b/elixir/apps/domain/lib/domain/devices/device/changeset.ex index 449ded4be..5bce2d96f 100644 --- a/elixir/apps/domain/lib/domain/devices/device/changeset.ex +++ b/elixir/apps/domain/lib/domain/devices/device/changeset.ex @@ -19,7 +19,7 @@ defmodule Domain.Devices.Device.Changeset do def upsert_on_conflict, do: {:replace, @conflict_replace_fields} - def upsert_changeset(%Auth.Identity{} = identity, %Auth.Context{} = context, attrs) do + def upsert(%Auth.Identity{} = identity, %Auth.Context{} = context, attrs) do %Devices.Device{} |> cast(attrs, @upsert_fields) |> put_default_value(:name, &generate_name/0) @@ -38,7 +38,7 @@ defmodule Domain.Devices.Device.Changeset do |> put_device_version() end - def finalize_upsert_changeset(%Devices.Device{} = device, ipv4, ipv6) do + def finalize_upsert(%Devices.Device{} = device, ipv4, ipv6) do device |> change() |> put_change(:ipv4, ipv4) @@ -47,14 +47,14 @@ defmodule Domain.Devices.Device.Changeset do |> unique_constraint(:ipv6, name: :devices_account_id_ipv6_index) end - def update_changeset(%Devices.Device{} = device, attrs) do + def update(%Devices.Device{} = device, attrs) do device |> cast(attrs, @update_fields) - |> changeset() |> validate_required(@required_fields) + |> changeset() end - def delete_changeset(%Devices.Device{} = device) do + def delete(%Devices.Device{} = device) do device |> change() |> put_default_value(:deleted_at, DateTime.utc_now()) @@ -68,6 +68,7 @@ defmodule Domain.Devices.Device.Changeset do |> unique_constraint([:actor_id, :name]) |> unique_constraint([:actor_id, :public_key]) |> unique_constraint(:external_id) + |> unique_constraint(:name, name: :devices_account_id_actor_id_name_index) end defp put_device_version(changeset) do diff --git a/elixir/apps/domain/lib/domain/gateways.ex b/elixir/apps/domain/lib/domain/gateways.ex index 2600832df..f455bccb8 100644 --- a/elixir/apps/domain/lib/domain/gateways.ex +++ b/elixir/apps/domain/lib/domain/gateways.ex @@ -102,7 +102,7 @@ defmodule Domain.Gateways do def create_group(attrs, %Auth.Subject{} = subject) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()) do subject.account - |> Group.Changeset.create_changeset(attrs, subject) + |> Group.Changeset.create(attrs, subject) |> Repo.insert() end end @@ -110,14 +110,14 @@ defmodule Domain.Gateways do def change_group(%Group{} = group, attrs \\ %{}) do group |> Repo.preload(:account) - |> Group.Changeset.update_changeset(attrs) + |> Group.Changeset.update(attrs) end def update_group(%Group{} = group, attrs, %Auth.Subject{} = subject) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()) do group |> Repo.preload(:account) - |> Group.Changeset.update_changeset(attrs) + |> Group.Changeset.update(attrs) |> Repo.update() end end @@ -132,12 +132,12 @@ defmodule Domain.Gateways do Token.Query.by_group_id(group.id) |> Repo.all() |> Enum.each(fn token -> - Token.Changeset.delete_changeset(token) + Token.Changeset.delete(token) |> Repo.update!() end) group - |> Group.Changeset.delete_changeset() + |> Group.Changeset.delete() end ) end @@ -149,7 +149,7 @@ defmodule Domain.Gateways do |> Repo.fetch_and_update( with: fn token -> if Domain.Crypto.equal?(secret, token.hash) do - Token.Changeset.use_changeset(token) + Token.Changeset.use(token) else :not_found end @@ -270,11 +270,11 @@ defmodule Domain.Gateways do end def change_gateway(%Gateway{} = gateway, attrs \\ %{}) do - Gateway.Changeset.update_changeset(gateway, attrs) + Gateway.Changeset.update(gateway, attrs) end def upsert_gateway(%Token{} = token, attrs) do - changeset = Gateway.Changeset.upsert_changeset(token, attrs) + changeset = Gateway.Changeset.upsert(token, attrs) Ecto.Multi.new() |> Ecto.Multi.insert(:gateway, changeset, @@ -286,7 +286,7 @@ defmodule Domain.Gateways do |> resolve_address_multi(:ipv6) |> Ecto.Multi.update(:gateway_with_address, fn %{gateway: %Gateway{} = gateway, ipv4: ipv4, ipv6: ipv6} -> - Gateway.Changeset.finalize_upsert_changeset(gateway, ipv4, ipv6) + Gateway.Changeset.finalize_upsert(gateway, ipv4, ipv6) end) |> Repo.transaction() |> case do @@ -309,7 +309,7 @@ defmodule Domain.Gateways do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()) do Gateway.Query.by_id(gateway.id) |> Authorizer.for_subject(subject) - |> Repo.fetch_and_update(with: &Gateway.Changeset.update_changeset(&1, attrs)) + |> Repo.fetch_and_update(with: &Gateway.Changeset.update(&1, attrs)) |> case do {:ok, gateway} -> {:ok, preload_online_status(gateway)} @@ -324,7 +324,7 @@ defmodule Domain.Gateways do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()) do Gateway.Query.by_id(gateway.id) |> Authorizer.for_subject(subject) - |> Repo.fetch_and_update(with: &Gateway.Changeset.delete_changeset/1) + |> Repo.fetch_and_update(with: &Gateway.Changeset.delete/1) |> case do {:ok, gateway} -> {:ok, preload_online_status(gateway)} diff --git a/elixir/apps/domain/lib/domain/gateways/authorizer.ex b/elixir/apps/domain/lib/domain/gateways/authorizer.ex index 7d39a9715..b9c53d421 100644 --- a/elixir/apps/domain/lib/domain/gateways/authorizer.ex +++ b/elixir/apps/domain/lib/domain/gateways/authorizer.ex @@ -9,7 +9,8 @@ defmodule Domain.Gateways.Authorizer do def list_permissions_for_role(:account_admin_user) do [ - manage_gateways_permission() + manage_gateways_permission(), + connect_gateways_permission() ] end diff --git a/elixir/apps/domain/lib/domain/gateways/gateway/changeset.ex b/elixir/apps/domain/lib/domain/gateways/gateway/changeset.ex index 47f20c0b8..e46fc932c 100644 --- a/elixir/apps/domain/lib/domain/gateways/gateway/changeset.ex +++ b/elixir/apps/domain/lib/domain/gateways/gateway/changeset.ex @@ -20,7 +20,7 @@ defmodule Domain.Gateways.Gateway.Changeset do def upsert_on_conflict, do: {:replace, @conflict_replace_fields} - def upsert_changeset(%Gateways.Token{} = token, attrs) do + def upsert(%Gateways.Token{} = token, attrs) do %Gateways.Gateway{} |> cast(attrs, @upsert_fields) |> put_default_value(:name_suffix, fn -> Domain.Crypto.rand_string(5) end) @@ -40,21 +40,21 @@ defmodule Domain.Gateways.Gateway.Changeset do |> assoc_constraint(:token) end - def finalize_upsert_changeset(%Gateways.Gateway{} = gateway, ipv4, ipv6) do + def finalize_upsert(%Gateways.Gateway{} = gateway, ipv4, ipv6) do gateway |> change() |> put_change(:ipv4, ipv4) |> put_change(:ipv6, ipv6) end - def update_changeset(%Gateways.Gateway{} = gateway, attrs) do + def update(%Gateways.Gateway{} = gateway, attrs) do gateway |> cast(attrs, @update_fields) |> changeset() |> validate_required(@required_fields) end - def delete_changeset(%Gateways.Gateway{} = gateway) do + def delete(%Gateways.Gateway{} = gateway) do gateway |> change() |> put_default_value(:deleted_at, DateTime.utc_now()) diff --git a/elixir/apps/domain/lib/domain/gateways/group/changeset.ex b/elixir/apps/domain/lib/domain/gateways/group/changeset.ex index fd960398a..f6c09acf4 100644 --- a/elixir/apps/domain/lib/domain/gateways/group/changeset.ex +++ b/elixir/apps/domain/lib/domain/gateways/group/changeset.ex @@ -5,7 +5,7 @@ defmodule Domain.Gateways.Group.Changeset do @fields ~w[name_prefix tags]a - def create_changeset(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do + def create(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do %Gateways.Group{account: account} |> changeset(attrs) |> put_change(:account_id, account.id) @@ -13,13 +13,13 @@ defmodule Domain.Gateways.Group.Changeset do |> put_change(:created_by_identity_id, subject.identity.id) |> cast_assoc(:tokens, with: fn _token, _attrs -> - Gateways.Token.Changeset.create_changeset(account, subject) + Gateways.Token.Changeset.create(account, subject) end, required: true ) end - def update_changeset(%Gateways.Group{} = group, attrs) do + def update(%Gateways.Group{} = group, attrs) do changeset(group, attrs) end @@ -28,6 +28,7 @@ defmodule Domain.Gateways.Group.Changeset do |> cast(attrs, @fields) |> trim_change(:name_prefix) |> put_default_value(:name_prefix, &Domain.NameGenerator.generate/0) + |> validate_required(@fields) |> validate_length(:name_prefix, min: 1, max: 64) |> validate_length(:tags, min: 0, max: 128) |> validate_no_duplicates(:tags) @@ -38,11 +39,10 @@ defmodule Domain.Gateways.Group.Changeset do [] end end) - |> validate_required(@fields) |> unique_constraint(:name_prefix, name: :gateway_groups_account_id_name_prefix_index) end - def delete_changeset(%Gateways.Group{} = group) do + def delete(%Gateways.Group{} = group) do group |> change() |> put_default_value(:deleted_at, DateTime.utc_now()) diff --git a/elixir/apps/domain/lib/domain/gateways/token/changeset.ex b/elixir/apps/domain/lib/domain/gateways/token/changeset.ex index 0a8f02e71..9654772dd 100644 --- a/elixir/apps/domain/lib/domain/gateways/token/changeset.ex +++ b/elixir/apps/domain/lib/domain/gateways/token/changeset.ex @@ -4,7 +4,7 @@ defmodule Domain.Gateways.Token.Changeset do alias Domain.Accounts alias Domain.Gateways - def create_changeset(%Accounts.Account{} = account, %Auth.Subject{} = subject) do + def create(%Accounts.Account{} = account, %Auth.Subject{} = subject) do %Gateways.Token{} |> change() |> put_change(:account_id, account.id) @@ -16,15 +16,15 @@ defmodule Domain.Gateways.Token.Changeset do |> put_change(:created_by_identity_id, subject.identity.id) end - def use_changeset(%Gateways.Token{} = token) do + def use(%Gateways.Token{} = token) do # TODO: While we don't have token rotation implemented, the tokens are all multi-use - # delete_changeset(token) + # delete(token) token |> change() end - def delete_changeset(%Gateways.Token{} = token) do + def delete(%Gateways.Token{} = token) do token |> change() |> put_default_value(:deleted_at, DateTime.utc_now()) diff --git a/elixir/apps/domain/lib/domain/jobs/executors/global.ex b/elixir/apps/domain/lib/domain/jobs/executors/global.ex index bb6a0e4f8..409d765fa 100644 --- a/elixir/apps/domain/lib/domain/jobs/executors/global.ex +++ b/elixir/apps/domain/lib/domain/jobs/executors/global.ex @@ -156,6 +156,12 @@ defmodule Domain.Jobs.Executors.Global do end defp execute_handler(module, function, config) do + Logger.metadata( + job_runner: __MODULE__, + job_execution_id: Ecto.UUID.generate(), + job_callback: "#{module}.#{function}/1" + ) + _ = apply(module, function, [config]) :ok end diff --git a/elixir/apps/domain/lib/domain/network.ex b/elixir/apps/domain/lib/domain/network.ex index 7c56414ab..ce7787aaf 100644 --- a/elixir/apps/domain/lib/domain/network.ex +++ b/elixir/apps/domain/lib/domain/network.ex @@ -22,7 +22,7 @@ defmodule Domain.Network do address = Address.Query.next_available_address(account_id, cidr, offset) |> Domain.Repo.one!() - |> Address.Changeset.create_changeset(account_id) + |> Address.Changeset.create(account_id) |> Repo.insert!() address.address diff --git a/elixir/apps/domain/lib/domain/network/address/changeset.ex b/elixir/apps/domain/lib/domain/network/address/changeset.ex index 6465fbecb..f590ad369 100644 --- a/elixir/apps/domain/lib/domain/network/address/changeset.ex +++ b/elixir/apps/domain/lib/domain/network/address/changeset.ex @@ -2,7 +2,7 @@ defmodule Domain.Network.Address.Changeset do use Domain, :changeset alias Domain.Network.Address - def create_changeset(address, account_id) do + def create(address, account_id) do %Address{} |> change() |> put_change(:address, address) diff --git a/elixir/apps/domain/lib/domain/policies.ex b/elixir/apps/domain/lib/domain/policies.ex index 74aa90a16..2f3608539 100644 --- a/elixir/apps/domain/lib/domain/policies.ex +++ b/elixir/apps/domain/lib/domain/policies.ex @@ -53,7 +53,7 @@ defmodule Domain.Policies do {:one_of, [Authorizer.manage_policies_permission()]} with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do - Policy.Changeset.create_changeset(attrs, subject) + Policy.Changeset.create(attrs, subject) |> Repo.insert() end end @@ -64,7 +64,7 @@ defmodule Domain.Policies do with :ok <- Auth.ensure_has_permissions(subject, required_permissions), :ok <- ensure_has_access_to(subject, policy) do - Policy.Changeset.update_changeset(policy, attrs) + Policy.Changeset.update(policy, attrs) |> Repo.update() end end @@ -76,7 +76,7 @@ defmodule Domain.Policies do with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do Policy.Query.by_id(policy.id) |> Authorizer.for_subject(subject) - |> Repo.fetch_and_update(with: &Policy.Changeset.delete_changeset/1) + |> Repo.fetch_and_update(with: &Policy.Changeset.delete/1) end end diff --git a/elixir/apps/domain/lib/domain/policies/authorizer.ex b/elixir/apps/domain/lib/domain/policies/authorizer.ex index 7cc463f58..c66986ae1 100644 --- a/elixir/apps/domain/lib/domain/policies/authorizer.ex +++ b/elixir/apps/domain/lib/domain/policies/authorizer.ex @@ -8,7 +8,8 @@ defmodule Domain.Policies.Authorizer do @impl Domain.Auth.Authorizer def list_permissions_for_role(:account_admin_user) do [ - manage_policies_permission() + manage_policies_permission(), + view_available_policies_permission() ] end diff --git a/elixir/apps/domain/lib/domain/policies/policy/changeset.ex b/elixir/apps/domain/lib/domain/policies/policy/changeset.ex index 7a9d922fe..7cf1430e8 100644 --- a/elixir/apps/domain/lib/domain/policies/policy/changeset.ex +++ b/elixir/apps/domain/lib/domain/policies/policy/changeset.ex @@ -7,7 +7,7 @@ defmodule Domain.Policies.Policy.Changeset do @update_fields ~w[name]a @required_fields @fields - def create_changeset(attrs, %Auth.Subject{} = subject) do + def create(attrs, %Auth.Subject{} = subject) do %Policy{} |> cast(attrs, @fields) |> validate_required(@required_fields) @@ -17,14 +17,14 @@ defmodule Domain.Policies.Policy.Changeset do |> put_change(:created_by_identity_id, subject.identity.id) end - def update_changeset(%Policy{} = policy, attrs) do + def update(%Policy{} = policy, attrs) do policy |> cast(attrs, @update_fields) |> validate_required(@required_fields) |> changeset() end - def delete_changeset(%Policy{} = policy) do + def delete(%Policy{} = policy) do policy |> change() |> put_change(:deleted_at, DateTime.utc_now()) diff --git a/elixir/apps/domain/lib/domain/relays.ex b/elixir/apps/domain/lib/domain/relays.ex index 12ed72332..7aa40e538 100644 --- a/elixir/apps/domain/lib/domain/relays.ex +++ b/elixir/apps/domain/lib/domain/relays.ex @@ -107,20 +107,20 @@ defmodule Domain.Relays do def create_group(attrs, %Auth.Subject{} = subject) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_relays_permission()) do subject.account - |> Group.Changeset.create_changeset(attrs, subject) + |> Group.Changeset.create(attrs, subject) |> Repo.insert() end end def create_global_group(attrs) do - Group.Changeset.create_changeset(attrs) + Group.Changeset.create(attrs) |> Repo.insert() end def change_group(%Group{} = group, attrs \\ %{}) do group |> Repo.preload(:account) - |> Group.Changeset.update_changeset(attrs) + |> Group.Changeset.update(attrs) end def update_group(group, attrs \\ %{}, subject) @@ -133,7 +133,7 @@ defmodule Domain.Relays do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_relays_permission()) do group |> Repo.preload(:account) - |> Group.Changeset.update_changeset(attrs, subject) + |> Group.Changeset.update(attrs, subject) |> Repo.update() end end @@ -153,12 +153,12 @@ defmodule Domain.Relays do Token.Query.by_group_id(group.id) |> Repo.all() |> Enum.each(fn token -> - Token.Changeset.delete_changeset(token) + Token.Changeset.delete(token) |> Repo.update!() end) group - |> Group.Changeset.delete_changeset() + |> Group.Changeset.delete() end ) end @@ -170,7 +170,7 @@ defmodule Domain.Relays do |> Repo.fetch_and_update( with: fn token -> if Domain.Crypto.equal?(secret, token.hash) do - Token.Changeset.use_changeset(token) + Token.Changeset.use(token) else :not_found end @@ -290,7 +290,7 @@ defmodule Domain.Relays do end def upsert_relay(%Token{} = token, attrs) do - changeset = Relay.Changeset.upsert_changeset(token, attrs) + changeset = Relay.Changeset.upsert(token, attrs) Ecto.Multi.new() |> Ecto.Multi.insert(:relay, changeset, @@ -309,7 +309,7 @@ defmodule Domain.Relays do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_relays_permission()) do Relay.Query.by_id(relay.id) |> Authorizer.for_subject(subject) - |> Repo.fetch_and_update(with: &Relay.Changeset.delete_changeset/1) + |> Repo.fetch_and_update(with: &Relay.Changeset.delete/1) end end diff --git a/elixir/apps/domain/lib/domain/relays/group/changeset.ex b/elixir/apps/domain/lib/domain/relays/group/changeset.ex index a1f42f008..533726ad4 100644 --- a/elixir/apps/domain/lib/domain/relays/group/changeset.ex +++ b/elixir/apps/domain/lib/domain/relays/group/changeset.ex @@ -6,24 +6,24 @@ defmodule Domain.Relays.Group.Changeset do @fields ~w[name]a - def create_changeset(attrs) do + def create(attrs) do %Relays.Group{} |> changeset(attrs) |> cast_assoc(:tokens, with: fn _token, _attrs -> - Relays.Token.Changeset.create_changeset() + Relays.Token.Changeset.create() end, required: true ) |> put_change(:created_by, :system) end - def create_changeset(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do + def create(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do %Relays.Group{account: account} |> changeset(attrs) |> cast_assoc(:tokens, with: fn _token, _attrs -> - Relays.Token.Changeset.create_changeset(account, subject) + Relays.Token.Changeset.create(account, subject) end, required: true ) @@ -32,21 +32,20 @@ defmodule Domain.Relays.Group.Changeset do |> put_change(:created_by_identity_id, subject.identity.id) end - def update_changeset(%Relays.Group{} = group, attrs, %Auth.Subject{} = subject) do + def update(%Relays.Group{} = group, attrs, %Auth.Subject{} = subject) do changeset(group, attrs) |> cast_assoc(:tokens, with: fn _token, _attrs -> - Relays.Token.Changeset.create_changeset(group.account, subject) - end, - required: true + Relays.Token.Changeset.create(group.account, subject) + end ) end - def update_changeset(%Relays.Group{} = group, attrs) do + def update(%Relays.Group{} = group, attrs) do changeset(group, attrs) |> cast_assoc(:tokens, with: fn _token, _attrs -> - Relays.Token.Changeset.create_changeset() + Relays.Token.Changeset.create() end, required: true ) @@ -57,13 +56,13 @@ defmodule Domain.Relays.Group.Changeset do |> cast(attrs, @fields) |> trim_change(:name) |> put_default_value(:name, &Domain.NameGenerator.generate/0) - |> validate_length(:name, min: 1, max: 64) |> validate_required(@fields) + |> validate_length(:name, min: 1, max: 64) |> unique_constraint(:name, name: :relay_groups_name_index) |> unique_constraint(:name, name: :relay_groups_account_id_name_index) end - def delete_changeset(%Relays.Group{} = group) do + def delete(%Relays.Group{} = group) do group |> change() |> put_default_value(:deleted_at, DateTime.utc_now()) diff --git a/elixir/apps/domain/lib/domain/relays/relay/changeset.ex b/elixir/apps/domain/lib/domain/relays/relay/changeset.ex index a13f843b1..aced73fcf 100644 --- a/elixir/apps/domain/lib/domain/relays/relay/changeset.ex +++ b/elixir/apps/domain/lib/domain/relays/relay/changeset.ex @@ -21,7 +21,7 @@ defmodule Domain.Relays.Relay.Changeset do def upsert_on_conflict, do: {:replace, @conflict_replace_fields} - def upsert_changeset(%Relays.Token{} = token, attrs) do + def upsert(%Relays.Token{} = token, attrs) do %Relays.Relay{} |> cast(attrs, @upsert_fields) |> validate_required(~w[last_seen_user_agent last_seen_remote_ip]a) @@ -39,7 +39,7 @@ defmodule Domain.Relays.Relay.Changeset do |> assoc_constraint(:token) end - def delete_changeset(%Relays.Relay{} = relay) do + def delete(%Relays.Relay{} = relay) do relay |> change() |> put_default_value(:deleted_at, DateTime.utc_now()) diff --git a/elixir/apps/domain/lib/domain/relays/token/changeset.ex b/elixir/apps/domain/lib/domain/relays/token/changeset.ex index 3718979ea..33c5f3e80 100644 --- a/elixir/apps/domain/lib/domain/relays/token/changeset.ex +++ b/elixir/apps/domain/lib/domain/relays/token/changeset.ex @@ -4,7 +4,7 @@ defmodule Domain.Relays.Token.Changeset do alias Domain.Accounts alias Domain.Relays - def create_changeset do + def create do %Relays.Token{} |> change() |> put_change(:value, Domain.Crypto.rand_string(64)) @@ -14,22 +14,22 @@ defmodule Domain.Relays.Token.Changeset do |> put_change(:created_by, :system) end - def create_changeset(%Accounts.Account{} = account, %Auth.Subject{} = subject) do - create_changeset() + def create(%Accounts.Account{} = account, %Auth.Subject{} = subject) do + create() |> put_change(:account_id, account.id) |> put_change(:created_by, :identity) |> put_change(:created_by_identity_id, subject.identity.id) end - def use_changeset(%Relays.Token{} = token) do + def use(%Relays.Token{} = token) do # TODO: While we don't have token rotation implemented, the tokens are all multi-use - # delete_changeset(token) + # delete(token) token |> change() end - def delete_changeset(%Relays.Token{} = token) do + def delete(%Relays.Token{} = token) do token |> change() |> put_default_value(:deleted_at, DateTime.utc_now()) diff --git a/elixir/apps/domain/lib/domain/repo.ex b/elixir/apps/domain/lib/domain/repo.ex index 1520d7117..31d0a024c 100644 --- a/elixir/apps/domain/lib/domain/repo.ex +++ b/elixir/apps/domain/lib/domain/repo.ex @@ -37,7 +37,10 @@ defmodule Domain.Repo do [{:with, changeset_fun :: (term() -> Ecto.Changeset.t())}], opts :: Keyword.t() ) :: - {:ok, Ecto.Schema.t()} | {:error, :not_found} | {:error, Ecto.Changeset.t()} + {:ok, Ecto.Schema.t()} + | {:error, :not_found} + | {:error, Ecto.Changeset.t()} + | {:error, term()} def fetch_and_update(queryable, [with: changeset_fun], opts \\ []) when is_function(changeset_fun, 1) do transaction(fn -> @@ -47,7 +50,7 @@ defmodule Domain.Repo do schema |> changeset_fun.() |> case do - %Ecto.Changeset{} = changeset -> update(changeset, opts) + %Ecto.Changeset{} = changeset -> update(changeset, mode: :savepoint) reason -> {:error, reason} end end @@ -65,4 +68,25 @@ defmodule Domain.Repo do def list(queryable, opts \\ []) do {:ok, __MODULE__.all(queryable, opts)} end + + @doc """ + Peek is used to fetch a preview of the a association for each of schemas. + + It takes list of schemas and queryable which is used to preload a few assocs along with + total count of assocs available as `%{id: schema.id, count: schema_counts.count, item: assocs}` map. + """ + def peek(queryable, schemas) do + ids = schemas |> Enum.map(& &1.id) |> Enum.uniq() + preview = Map.new(ids, fn id -> {id, %{count: 0, items: []}} end) + + preview = + queryable + |> all() + |> Enum.group_by(&{&1.id, &1.count}, & &1.item) + |> Enum.reduce(preview, fn {{id, count}, items}, acc -> + Map.put(acc, id, %{count: count, items: items}) + end) + + {:ok, preview} + end end diff --git a/elixir/apps/domain/lib/domain/resources.ex b/elixir/apps/domain/lib/domain/resources.ex index d4276da3d..91a292d11 100644 --- a/elixir/apps/domain/lib/domain/resources.ex +++ b/elixir/apps/domain/lib/domain/resources.ex @@ -98,7 +98,7 @@ defmodule Domain.Resources do def create_resource(attrs, %Auth.Subject{} = subject) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_resources_permission()) do - changeset = Resource.Changeset.create_changeset(subject.account, attrs, subject) + changeset = Resource.Changeset.create(subject.account, attrs, subject) Ecto.Multi.new() |> Ecto.Multi.insert(:resource, changeset, returning: true) @@ -106,7 +106,7 @@ defmodule Domain.Resources do |> resolve_address_multi(:ipv6) |> Ecto.Multi.update(:resource_with_address, fn %{resource: %Resource{} = resource, ipv4: ipv4, ipv6: ipv6} -> - Resource.Changeset.finalize_create_changeset(resource, ipv4, ipv6) + Resource.Changeset.finalize_create(resource, ipv4, ipv6) end) |> Repo.transaction() |> case do @@ -145,7 +145,7 @@ defmodule Domain.Resources do def update_resource(%Resource{} = resource, attrs, %Auth.Subject{} = subject) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_resources_permission()) do resource - |> Resource.Changeset.update_changeset(attrs, subject) + |> Resource.Changeset.update(attrs, subject) |> Repo.update() |> case do {:ok, resource} -> @@ -167,7 +167,7 @@ defmodule Domain.Resources do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_resources_permission()) do Resource.Query.by_id(resource.id) |> Authorizer.for_subject(subject) - |> Repo.fetch_and_update(with: &Resource.Changeset.delete_changeset/1) + |> Repo.fetch_and_update(with: &Resource.Changeset.delete/1) |> case do {:ok, resource} -> # Phoenix.PubSub.broadcast( diff --git a/elixir/apps/domain/lib/domain/resources/authorizer.ex b/elixir/apps/domain/lib/domain/resources/authorizer.ex index 29d49b9e6..1791f0b7c 100644 --- a/elixir/apps/domain/lib/domain/resources/authorizer.ex +++ b/elixir/apps/domain/lib/domain/resources/authorizer.ex @@ -8,7 +8,8 @@ defmodule Domain.Resources.Authorizer do @impl Domain.Auth.Authorizer def list_permissions_for_role(:account_admin_user) do [ - manage_resources_permission() + manage_resources_permission(), + view_available_resources_permission() ] end diff --git a/elixir/apps/domain/lib/domain/resources/resource/changeset.ex b/elixir/apps/domain/lib/domain/resources/resource/changeset.ex index 61ec84c1f..410aff8b0 100644 --- a/elixir/apps/domain/lib/domain/resources/resource/changeset.ex +++ b/elixir/apps/domain/lib/domain/resources/resource/changeset.ex @@ -7,7 +7,7 @@ defmodule Domain.Resources.Resource.Changeset do @update_fields ~w[name]a @required_fields ~w[address type]a - def create_changeset(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do + def create(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do %Resource{} |> cast(attrs, @fields) |> validate_required(@required_fields) @@ -22,7 +22,7 @@ defmodule Domain.Resources.Resource.Changeset do |> put_change(:created_by_identity_id, subject.identity.id) end - def finalize_create_changeset(%Resource{} = resource, ipv4, ipv6) do + def finalize_create(%Resource{} = resource, ipv4, ipv6) do resource |> change() |> put_change(:ipv4, ipv4) @@ -73,7 +73,7 @@ defmodule Domain.Resources.Resource.Changeset do end end - def update_changeset(%Resource{} = resource, attrs, %Auth.Subject{} = subject) do + def update(%Resource{} = resource, attrs, %Auth.Subject{} = subject) do resource |> cast(attrs, @update_fields) |> validate_required(@required_fields) @@ -99,7 +99,7 @@ defmodule Domain.Resources.Resource.Changeset do ) end - def delete_changeset(%Resource{} = resource) do + def delete(%Resource{} = resource) do resource |> change() |> put_default_value(:deleted_at, DateTime.utc_now()) diff --git a/elixir/apps/domain/lib/domain/validator.ex b/elixir/apps/domain/lib/domain/validator.ex index 800e258bd..e6272acaa 100644 --- a/elixir/apps/domain/lib/domain/validator.ex +++ b/elixir/apps/domain/lib/domain/validator.ex @@ -328,6 +328,16 @@ defmodule Domain.Validator do end) end + def validate_date(changeset, field, greater_than: greater_than) do + validate_change(changeset, field, fn _current_field, value -> + if Date.compare(value, greater_than) == :gt do + [] + else + [{field, "must be greater than #{inspect(greater_than)}"}] + end + end) + end + @doc """ Applies a validation function for every elements of the list. diff --git a/elixir/apps/domain/priv/repo/migrations/20230810165123_created_auth_identities_inserted_at.exs b/elixir/apps/domain/priv/repo/migrations/20230810165123_created_auth_identities_inserted_at.exs new file mode 100644 index 000000000..7595c1859 --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20230810165123_created_auth_identities_inserted_at.exs @@ -0,0 +1,9 @@ +defmodule Domain.Repo.Migrations.CreatedAuthIdentitiesInsertedAt do + use Ecto.Migration + + def change do + alter table(:auth_identities) do + timestamps(updated_at: false) + end + end +end diff --git a/elixir/apps/domain/priv/repo/migrations/20230810211513_add_actor_groups_created_by.exs b/elixir/apps/domain/priv/repo/migrations/20230810211513_add_actor_groups_created_by.exs new file mode 100644 index 000000000..fcc3456a3 --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20230810211513_add_actor_groups_created_by.exs @@ -0,0 +1,10 @@ +defmodule Domain.Repo.Migrations.AddActorGroupsCreatedBy do + use Ecto.Migration + + def change do + alter table(:actor_groups) do + add(:created_by, :string, null: false) + add(:created_by_identity_id, references(:auth_identities, type: :binary_id)) + end + end +end diff --git a/elixir/apps/domain/priv/repo/migrations/20230821214259_add_actors_last_synced_at.exs b/elixir/apps/domain/priv/repo/migrations/20230821214259_add_actors_last_synced_at.exs new file mode 100644 index 000000000..71b48db7e --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20230821214259_add_actors_last_synced_at.exs @@ -0,0 +1,9 @@ +defmodule Domain.Repo.Migrations.AddActorsLastSyncedAt do + use Ecto.Migration + + def change do + alter table(:actors) do + add(:last_synced_at, :utc_datetime_usec) + end + end +end diff --git a/elixir/apps/domain/priv/repo/seeds.exs b/elixir/apps/domain/priv/repo/seeds.exs index bc9b95519..59851b235 100644 --- a/elixir/apps/domain/priv/repo/seeds.exs +++ b/elixir/apps/domain/priv/repo/seeds.exs @@ -53,15 +53,15 @@ IO.puts("") adapter_config: %{} }) -{:ok, _oidc_provider} = +{:ok, oidc_provider} = Auth.create_provider(account, %{ - name: "Vault", + name: "OIDC", adapter: :openid_connect, adapter_config: %{ "client_id" => "CLIENT_ID", "client_secret" => "CLIENT_SECRET", "response_type" => "code", - "scope" => "openid email profile", + "scope" => "openid email name groups", "discovery_document_uri" => "https://common.auth0.com/.well-known/openid-configuration" } }) @@ -73,7 +73,7 @@ IO.puts("") adapter_config: %{} }) -{:ok, other_email_provider} = +{:ok, _other_email_provider} = Auth.create_provider(other_account, %{ name: "email", adapter: :email, @@ -91,36 +91,35 @@ unprivileged_actor_email = "firezone-unprivileged-1@localhost" admin_actor_email = "firezone@localhost" {:ok, unprivileged_actor} = - Actors.create_actor(email_provider, unprivileged_actor_email, %{ + Actors.create_actor(account, %{ type: :account_user, name: "Firezone Unprivileged" }) {:ok, admin_actor} = - Actors.create_actor(email_provider, admin_actor_email, %{ + Actors.create_actor(account, %{ type: :account_admin_user, name: "Firezone Admin" }) {:ok, service_account_actor} = - Actors.create_actor(token_provider, "backup-manager", %{ + Actors.create_actor(account, %{ "type" => :service_account, - "name" => "Backup Manager", - "provider" => %{ - expires_at: DateTime.utc_now() |> DateTime.add(365, :day) - } + "name" => "Backup Manager" + }) + +{:ok, unprivileged_actor_email_identity} = + Auth.create_identity(unprivileged_actor, email_provider, %{ + provider_identifier: unprivileged_actor_email }) {:ok, unprivileged_actor_userpass_identity} = - Auth.create_identity(unprivileged_actor, userpass_provider, unprivileged_actor_email, %{ - "password" => "Firezone1234", - "password_confirmation" => "Firezone1234" - }) - -{:ok, _admin_actor_userpass_identity} = - Auth.create_identity(admin_actor, userpass_provider, admin_actor_email, %{ - "password" => "Firezone1234", - "password_confirmation" => "Firezone1234" + Auth.create_identity(unprivileged_actor, userpass_provider, %{ + provider_identifier: unprivileged_actor_email, + provider_virtual_state: %{ + "password" => "Firezone1234", + "password_confirmation" => "Firezone1234" + } }) unprivileged_actor_userpass_identity = @@ -128,41 +127,64 @@ unprivileged_actor_userpass_identity = id: "7da7d1cd-111c-44a7-b5ac-4027b9d230e5" ) +{:ok, admin_actor_email_identity} = + Auth.create_identity(admin_actor, email_provider, %{ + provider_identifier: admin_actor_email + }) + +{:ok, _admin_actor_userpass_identity} = + Auth.create_identity(admin_actor, userpass_provider, %{ + provider_identifier: admin_actor_email, + provider_virtual_state: %{ + "password" => "Firezone1234", + "password_confirmation" => "Firezone1234" + } + }) + +{:ok, service_account_actor_token_identity} = + Auth.create_identity(service_account_actor, token_provider, %{ + provider_identifier: "tok-#{Ecto.UUID.generate()}", + provider_virtual_state: %{ + "expires_at" => DateTime.utc_now() |> DateTime.add(365, :day) + } + }) + # Other Account Users other_unprivileged_actor_email = "other-unprivileged-1@localhost" other_admin_actor_email = "other@localhost" {:ok, other_unprivileged_actor} = - Actors.create_actor(other_email_provider, other_unprivileged_actor_email, %{ + Actors.create_actor(other_account, %{ type: :account_user, name: "Other Unprivileged" }) {:ok, other_admin_actor} = - Actors.create_actor(other_email_provider, other_admin_actor_email, %{ + Actors.create_actor(other_account, %{ type: :account_admin_user, name: "Other Admin" }) {:ok, _other_unprivileged_actor_userpass_identity} = - Auth.create_identity( - other_unprivileged_actor, - other_userpass_provider, - other_unprivileged_actor_email, - %{ + Auth.create_identity(other_unprivileged_actor, other_userpass_provider, %{ + provider_identifier: other_unprivileged_actor_email, + provider_virtual_state: %{ "password" => "Firezone1234", "password_confirmation" => "Firezone1234" } - ) - -{:ok, _other_admin_actor_userpass_identity} = - Auth.create_identity(other_admin_actor, other_userpass_provider, other_admin_actor_email, %{ - "password" => "Firezone1234", - "password_confirmation" => "Firezone1234" }) -unprivileged_actor_token = hd(unprivileged_actor.identities).provider_virtual_state.sign_in_token -admin_actor_token = hd(admin_actor.identities).provider_virtual_state.sign_in_token +{:ok, _other_admin_actor_userpass_identity} = + Auth.create_identity(other_admin_actor, other_userpass_provider, %{ + provider_identifier: other_admin_actor_email, + provider_virtual_state: %{ + "password" => "Firezone1234", + "password_confirmation" => "Firezone1234" + } + }) + +unprivileged_actor_token = unprivileged_actor_email_identity.provider_virtual_state.sign_in_token +admin_actor_token = admin_actor_email_identity.provider_virtual_state.sign_in_token unprivileged_subject = Auth.build_subject( @@ -174,7 +196,7 @@ unprivileged_subject = admin_subject = Auth.build_subject( - hd(admin_actor.identities), + admin_actor_email_identity, nil, "iOS/12.5 (iPhone) connlib/0.7.412", {100, 64, 100, 58} @@ -190,11 +212,11 @@ for {type, login, password, email_token} <- [ IO.puts(" #{login}, #{type}, password: #{password}, email token: #{email_token} (exp in 15m)") end -service_account_identity = hd(service_account_actor.identities) -service_account_token = service_account_identity.provider_virtual_state.secret +service_account_token = service_account_actor_token_identity.provider_virtual_state.changes.secret IO.puts( - " #{service_account_identity.provider_identifier}, #{service_account_actor.type}, token: #{service_account_token}" + " #{service_account_actor_token_identity.provider_identifier}," <> + "#{service_account_actor.type}, token: #{service_account_token}" ) IO.puts("") @@ -228,7 +250,12 @@ IO.puts("Created Actor Groups: ") {:ok, eng_group} = Actors.create_group(%{name: "Engineering"}, admin_subject) {:ok, finance_group} = Actors.create_group(%{name: "Finance"}, admin_subject) -{:ok, all_group} = Actors.create_group(%{name: "All Employees"}, admin_subject) + +{:ok, all_group} = + Actors.create_group( + %{name: "All Employees", provider_id: oidc_provider.id, provider_identifier: "foo"}, + admin_subject + ) for group <- [eng_group, finance_group, all_group] do IO.puts(" Name: #{group.name} ID: #{group.id}") @@ -261,7 +288,7 @@ IO.puts("") relay_group = account - |> Relays.Group.Changeset.create_changeset( + |> Relays.Group.Changeset.create( %{name: "mycorp-aws-relays", tokens: [%{}]}, admin_subject ) @@ -296,7 +323,7 @@ IO.puts("") gateway_group = account - |> Gateways.Group.Changeset.create_changeset( + |> Gateways.Group.Changeset.create( %{name_prefix: "mycro-aws-gws", tags: ["aws", "in-da-cloud"], tokens: [%{}]}, admin_subject ) diff --git a/elixir/apps/domain/test/domain/accounts_test.exs b/elixir/apps/domain/test/domain/accounts_test.exs index d0b987270..8dfa74aff 100644 --- a/elixir/apps/domain/test/domain/accounts_test.exs +++ b/elixir/apps/domain/test/domain/accounts_test.exs @@ -2,14 +2,13 @@ defmodule Domain.AccountsTest do use Domain.DataCase, async: true import Domain.Accounts alias Domain.Accounts - alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures} describe "fetch_account_by_id/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -33,7 +32,7 @@ defmodule Domain.AccountsTest do end test "returns error when subject has no permission to view accounts", %{subject: subject} do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_account_by_id(Ecto.UUID.generate(), subject) == {:error, @@ -45,11 +44,11 @@ defmodule Domain.AccountsTest do describe "fetch_account_by_id_or_slug/2" do setup do account = - AccountsFixtures.create_account(slug: "follow_the_#{System.unique_integer([:positive])}") + Fixtures.Accounts.create_account(slug: "follow_the_#{System.unique_integer([:positive])}") - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -70,7 +69,7 @@ defmodule Domain.AccountsTest do end test "returns error when subject has no permission to view accounts", %{subject: subject} do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_account_by_id_or_slug(Ecto.UUID.generate(), subject) == {:error, @@ -87,7 +86,7 @@ defmodule Domain.AccountsTest do test "returns account when account exists" do account = - AccountsFixtures.create_account(slug: "follow_the_#{System.unique_integer([:positive])}") + Fixtures.Accounts.create_account(slug: "follow_the_#{System.unique_integer([:positive])}") assert {:ok, fetched_account} = fetch_account_by_id_or_slug(account.id) assert fetched_account.id == account.id @@ -107,7 +106,7 @@ defmodule Domain.AccountsTest do end test "returns account" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() assert {:ok, returned_account} = fetch_account_by_id(account.id) assert returned_account.id == account.id end @@ -115,14 +114,14 @@ defmodule Domain.AccountsTest do describe "ensure_has_access_to/2" do test "returns :ok if subject has access to the account" do - subject = AuthFixtures.create_subject() + subject = Fixtures.Auth.create_subject() assert ensure_has_access_to(subject, subject.account) == :ok end test "returns :error if subject has no access to the account" do - account = AccountsFixtures.create_account() - subject = AuthFixtures.create_subject() + account = Fixtures.Accounts.create_account() + subject = Fixtures.Auth.create_subject() assert ensure_has_access_to(subject, account) == {:error, :unauthorized} end diff --git a/elixir/apps/domain/test/domain/actors_test.exs b/elixir/apps/domain/test/domain/actors_test.exs index 79c667667..ef1b8a581 100644 --- a/elixir/apps/domain/test/domain/actors_test.exs +++ b/elixir/apps/domain/test/domain/actors_test.exs @@ -3,14 +3,13 @@ defmodule Domain.ActorsTest do import Domain.Actors alias Domain.Auth alias Domain.Actors - alias Domain.{AccountsFixtures, AuthFixtures, ActorsFixtures} describe "fetch_group_by_id/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -27,7 +26,7 @@ defmodule Domain.ActorsTest do test "does not return groups from other accounts", %{ subject: subject } do - group = ActorsFixtures.create_group() + group = Fixtures.Actors.create_group() assert fetch_group_by_id(group.id, subject) == {:error, :not_found} end @@ -36,14 +35,14 @@ defmodule Domain.ActorsTest do subject: subject } do group = - ActorsFixtures.create_group(account: account) - |> ActorsFixtures.delete_group() + Fixtures.Actors.create_group(account: account) + |> Fixtures.Actors.delete_group() assert fetch_group_by_id(group.id, subject) == {:error, :not_found} end test "returns group by id", %{account: account, subject: subject} do - group = ActorsFixtures.create_group(account: account) + group = Fixtures.Actors.create_group(account: account) assert {:ok, fetched_group} = fetch_group_by_id(group.id, subject) assert fetched_group.id == group.id end @@ -52,7 +51,7 @@ defmodule Domain.ActorsTest do account: account, subject: subject } do - group = ActorsFixtures.create_group(account: account) + group = Fixtures.Actors.create_group(account: account) assert {:ok, fetched_group} = fetch_group_by_id(group.id, subject) assert fetched_group.id == group.id end @@ -65,7 +64,7 @@ defmodule Domain.ActorsTest do test "returns error when subject has no permission to view groups", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_group_by_id(Ecto.UUID.generate(), subject) == {:error, @@ -76,10 +75,10 @@ defmodule Domain.ActorsTest do describe "list_groups/1" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -96,7 +95,7 @@ defmodule Domain.ActorsTest do test "does not list groups from other accounts", %{ subject: subject } do - ActorsFixtures.create_group() + Fixtures.Actors.create_group() assert list_groups(subject) == {:ok, []} end @@ -104,8 +103,8 @@ defmodule Domain.ActorsTest do account: account, subject: subject } do - ActorsFixtures.create_group(account: account) - |> ActorsFixtures.delete_group() + Fixtures.Actors.create_group(account: account) + |> Fixtures.Actors.delete_group() assert list_groups(subject) == {:ok, []} end @@ -114,9 +113,9 @@ defmodule Domain.ActorsTest do account: account, subject: subject } do - ActorsFixtures.create_group(account: account) - ActorsFixtures.create_group(account: account) - ActorsFixtures.create_group() + Fixtures.Actors.create_group(account: account) + Fixtures.Actors.create_group(account: account) + Fixtures.Actors.create_group() assert {:ok, groups} = list_groups(subject) assert length(groups) == 2 @@ -125,7 +124,7 @@ defmodule Domain.ActorsTest do test "returns error when subject has no permission to manage groups", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert list_groups(subject) == {:error, @@ -134,6 +133,548 @@ defmodule Domain.ActorsTest do end end + describe "peek_group_actors/3" do + setup do + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) + + %{ + account: account, + actor: actor, + identity: identity, + subject: subject + } + end + + test "returns count of actors per group and first 3 actors", %{ + account: account, + subject: subject + } do + group1 = Fixtures.Actors.create_group(account: account) + Fixtures.Actors.create_membership(account: account, group: group1) + Fixtures.Actors.create_membership(account: account, group: group1) + Fixtures.Actors.create_membership(account: account, group: group1) + Fixtures.Actors.create_membership(account: account, group: group1) + + group2 = Fixtures.Actors.create_group(account: account) + + assert {:ok, peek} = peek_group_actors([group1, group2], 3, subject) + + assert length(Map.keys(peek)) == 2 + + assert peek[group1.id].count == 4 + assert length(peek[group1.id].items) == 3 + assert [%Actors.Actor{} | _] = peek[group1.id].items + + assert peek[group2.id].count == 0 + assert Enum.empty?(peek[group2.id].items) + end + + test "returns count of actors per group and first LIMIT actors", %{ + account: account, + subject: subject + } do + group = Fixtures.Actors.create_group(account: account) + Fixtures.Actors.create_membership(account: account, group: group) + Fixtures.Actors.create_membership(account: account, group: group) + + other_group = Fixtures.Actors.create_group(account: account) + Fixtures.Actors.create_membership(account: account, group: other_group) + + assert {:ok, peek} = peek_group_actors([group], 1, subject) + assert length(peek[group.id].items) == 1 + end + + test "ignores deleted actors", %{ + account: account, + subject: subject + } do + group = Fixtures.Actors.create_group(account: account) + actor = Fixtures.Actors.create_actor(account: account) |> Fixtures.Actors.delete() + Fixtures.Actors.create_membership(account: account, group: group, actor: actor) + Fixtures.Actors.create_membership(account: account, group: group) + Fixtures.Actors.create_membership(account: account, group: group) + Fixtures.Actors.create_membership(account: account, group: group) + Fixtures.Actors.create_membership(account: account, group: group) + + assert {:ok, peek} = peek_group_actors([group], 3, subject) + assert peek[group.id].count == 4 + assert length(peek[group.id].items) == 3 + end + + test "ignores other groups", %{ + account: account, + subject: subject + } do + Fixtures.Actors.create_membership(account: account) + Fixtures.Actors.create_membership(account: account) + + group = Fixtures.Actors.create_group(account: account) + + assert {:ok, peek} = peek_group_actors([group], 1, subject) + assert peek[group.id].count == 0 + assert Enum.empty?(peek[group.id].items) + end + + test "returns empty map on empty groups", %{subject: subject} do + assert peek_group_actors([], 1, subject) == {:ok, %{}} + end + + test "returns empty map on empty actors", %{account: account, subject: subject} do + group = Fixtures.Actors.create_group(account: account) + assert {:ok, peek} = peek_group_actors([group], 3, subject) + assert length(Map.keys(peek)) == 1 + assert peek[group.id].count == 0 + assert Enum.empty?(peek[group.id].items) + end + end + + describe "peek_actor_groups/3" do + setup do + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) + + %{ + account: account, + actor: actor, + identity: identity, + subject: subject + } + end + + test "returns count of actors per group and first 3 actors", %{ + account: account, + subject: subject + } do + actor1 = Fixtures.Actors.create_actor(account: account) + Fixtures.Actors.create_membership(account: account, actor: actor1) + Fixtures.Actors.create_membership(account: account, actor: actor1) + Fixtures.Actors.create_membership(account: account, actor: actor1) + Fixtures.Actors.create_membership(account: account, actor: actor1) + + actor2 = Fixtures.Actors.create_actor(account: account) + + assert {:ok, peek} = peek_actor_groups([actor1, actor2], 3, subject) + + assert length(Map.keys(peek)) == 2 + + assert peek[actor1.id].count == 4 + assert length(peek[actor1.id].items) == 3 + assert [%Actors.Group{} | _] = peek[actor1.id].items + + assert peek[actor2.id].count == 0 + assert Enum.empty?(peek[actor2.id].items) + end + + test "returns count of actors per group and first LIMIT actors", %{ + account: account, + subject: subject + } do + actor = Fixtures.Actors.create_actor(account: account) + Fixtures.Actors.create_membership(account: account, actor: actor) + Fixtures.Actors.create_membership(account: account, actor: actor) + + other_actor = Fixtures.Actors.create_actor(account: account) + Fixtures.Actors.create_membership(account: account, actor: other_actor) + + assert {:ok, peek} = peek_actor_groups([actor], 1, subject) + assert length(peek[actor.id].items) == 1 + end + + test "ignores deleted groups", %{ + account: account, + subject: subject + } do + actor = Fixtures.Actors.create_actor(account: account) + group = Fixtures.Actors.create_group(account: account) |> Fixtures.Actors.delete() + Fixtures.Actors.create_membership(account: account, group: group, actor: actor) + Fixtures.Actors.create_membership(account: account, group: group) + + assert {:ok, peek} = peek_actor_groups([actor], 3, subject) + assert peek[actor.id].count == 0 + assert Enum.empty?(peek[actor.id].items) + end + + test "ignores other groups", %{ + account: account, + subject: subject + } do + Fixtures.Actors.create_membership(account: account) + Fixtures.Actors.create_membership(account: account) + + actor = Fixtures.Actors.create_actor(account: account) + + assert {:ok, peek} = peek_actor_groups([actor], 1, subject) + assert peek[actor.id].count == 0 + assert Enum.empty?(peek[actor.id].items) + end + + test "returns empty map on empty actors", %{subject: subject} do + assert peek_actor_groups([], 1, subject) == {:ok, %{}} + end + + test "returns empty map on empty groups", %{account: account, subject: subject} do + actor = Fixtures.Actors.create_actor(account: account) + assert {:ok, peek} = peek_actor_groups([actor], 3, subject) + assert length(Map.keys(peek)) == 1 + assert peek[actor.id].count == 0 + assert Enum.empty?(peek[actor.id].items) + end + end + + describe "sync_provider_groups_multi/2" do + setup do + account = Fixtures.Accounts.create_account() + + {provider, bypass} = + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) + + %{account: account, provider: provider, bypass: bypass} + end + + test "creates new groups", %{provider: provider} do + attrs_list = [ + %{"name" => "Group:Infrastructure", "provider_identifier" => "G:GROUP_ID1"}, + %{"name" => "OrgUnit:Engineering", "provider_identifier" => "OU:OU_ID1"} + ] + + multi = sync_provider_groups_multi(provider, attrs_list) + + assert {:ok, + %{ + plan_groups: {upsert, []}, + delete_groups: {0, nil}, + upsert_groups: [_group1, _group2] + }} = Repo.transaction(multi) + + assert Enum.all?(["G:GROUP_ID1", "OU:OU_ID1"], &(&1 in upsert)) + groups = Repo.all(Actors.Group) + group_names = Enum.map(attrs_list, & &1["name"]) + assert length(groups) == 2 + + for group <- groups do + assert group.inserted_at + assert group.updated_at + + assert group.created_by == :provider + assert group.provider_id == provider.id + + assert group.name in group_names + end + end + + test "updates existing groups", %{account: account, provider: provider} do + group1 = + Fixtures.Actors.create_group( + account: account, + provider: provider, + provider_identifier: "G:GROUP_ID1" + ) + + _group2 = + Fixtures.Actors.create_group( + account: account, + provider: provider, + provider_identifier: "OU:OU_ID1" + ) + + attrs_list = [ + %{"name" => "Group:Infrastructure", "provider_identifier" => "G:GROUP_ID1"}, + %{"name" => "OrgUnit:Engineering", "provider_identifier" => "OU:OU_ID1"} + ] + + multi = sync_provider_groups_multi(provider, attrs_list) + + assert {:ok, + %{ + plan_groups: {upsert, []}, + delete_groups: {0, nil}, + upsert_groups: [_group1, _group2] + }} = Repo.transaction(multi) + + assert Enum.all?(["G:GROUP_ID1", "OU:OU_ID1"], &(&1 in upsert)) + assert Repo.aggregate(Actors.Group, :count) == 2 + + groups = Repo.all(Actors.Group) + group_names = Enum.map(attrs_list, & &1["name"]) + assert length(groups) == 2 + + for group <- groups do + assert group.name in group_names + assert group.inserted_at + assert group.updated_at + assert group.provider_id == provider.id + assert group.created_by == group1.created_by + end + end + + test "deletes removed groups", %{account: account, provider: provider} do + Fixtures.Actors.create_group( + account: account, + provider: provider, + provider_identifier: "G:GROUP_ID1" + ) + + Fixtures.Actors.create_group( + account: account, + provider: provider, + provider_identifier: "OU:OU_ID1" + ) + + attrs_list = [] + + multi = sync_provider_groups_multi(provider, attrs_list) + + assert {:ok, + %{ + plan_groups: {[], delete}, + delete_groups: {2, nil}, + upsert_groups: [] + }} = Repo.transaction(multi) + + assert Enum.all?(["G:GROUP_ID1", "OU:OU_ID1"], &(&1 in delete)) + assert Repo.aggregate(Actors.Group, :count) == 2 + assert Repo.aggregate(Actors.Group.Query.all(), :count) == 0 + end + + test "ignores groups that are not synced from the provider", %{ + account: account, + provider: provider + } do + {other_provider, _bypass} = + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) + + Fixtures.Actors.create_group( + account: account, + provider: other_provider, + provider_identifier: "G:GROUP_ID1" + ) + + Fixtures.Actors.create_group( + account: account, + provider_identifier: "OU:OU_ID1" + ) + + attrs_list = [] + + multi = sync_provider_groups_multi(provider, attrs_list) + + assert Repo.transaction(multi) == + {:ok, + %{ + groups: [], + plan_groups: {[], []}, + delete_groups: {0, nil}, + upsert_groups: [] + }} + end + end + + describe "sync_provider_memberships_multi/2" do + setup do + account = Fixtures.Accounts.create_account() + + {provider, bypass} = + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) + + group1 = + Fixtures.Actors.create_group( + account: account, + provider: provider, + provider_identifier: "G:GROUP_ID1" + ) + + group2 = + Fixtures.Actors.create_group( + account: account, + provider: provider, + provider_identifier: "OU:OU_ID1" + ) + + identity1 = + Fixtures.Auth.create_identity( + account: account, + provider: provider, + provider_identifier: "USER_ID1" + ) + + identity2 = + Fixtures.Auth.create_identity( + account: account, + provider: provider, + provider_identifier: "USER_ID2" + ) + + %{ + account: account, + provider: provider, + group1: group1, + group2: group2, + identity1: identity1, + identity2: identity2, + bypass: bypass + } + end + + test "creates new memberships", %{ + provider: provider, + group1: group1, + group2: group2, + identity1: identity1, + identity2: identity2 + } do + tuples_list = [ + {group1.provider_identifier, identity1.provider_identifier}, + {group2.provider_identifier, identity2.provider_identifier} + ] + + multi = + Ecto.Multi.new() + |> Ecto.Multi.put(:insert_identities, [identity1]) + |> Ecto.Multi.put(:identities, [identity2]) + |> Ecto.Multi.put(:groups, [group1]) + |> Ecto.Multi.put(:upsert_groups, [group2]) + |> sync_provider_memberships_multi(provider, tuples_list) + + assert {:ok, + %{ + plan_memberships: {insert, []}, + delete_memberships: {0, nil}, + upsert_memberships: [_membership1, _membership2] + }} = Repo.transaction(multi) + + assert {group1.id, identity1.actor_id} in insert + assert {group2.id, identity2.actor_id} in insert + + memberships = Repo.all(Actors.Membership) + assert length(memberships) == 2 + + for membership <- memberships do + assert {membership.group_id, membership.actor_id} in insert + end + end + + test "updates existing memberships", %{ + account: account, + provider: provider, + group1: group1, + group2: group2, + identity1: identity1, + identity2: identity2 + } do + Fixtures.Actors.create_membership( + account: account, + group: group1, + actor_id: identity1.actor_id + ) + + Fixtures.Actors.create_membership( + account: account, + group: group2, + actor_id: identity2.actor_id + ) + + tuples_list = [ + {group1.provider_identifier, identity1.provider_identifier}, + {group2.provider_identifier, identity2.provider_identifier} + ] + + multi = + Ecto.Multi.new() + |> Ecto.Multi.put(:insert_identities, [identity1]) + |> Ecto.Multi.put(:identities, [identity2]) + |> Ecto.Multi.put(:groups, [group1]) + |> Ecto.Multi.put(:upsert_groups, [group2]) + |> sync_provider_memberships_multi(provider, tuples_list) + + assert {:ok, + %{ + plan_memberships: {[], []}, + delete_memberships: {0, nil}, + upsert_memberships: [] + }} = Repo.transaction(multi) + + assert Repo.aggregate(Actors.Membership, :count) == 2 + assert Repo.aggregate(Actors.Membership.Query.all(), :count) == 2 + end + + test "deletes removed memberships", %{ + account: account, + provider: provider, + group1: group1, + group2: group2, + identity1: identity1, + identity2: identity2 + } do + Fixtures.Actors.create_membership( + account: account, + group: group1, + actor_id: identity1.actor_id + ) + + Fixtures.Actors.create_membership( + account: account, + group: group2, + actor_id: identity2.actor_id + ) + + tuples_list = [] + + multi = + Ecto.Multi.new() + |> Ecto.Multi.put(:insert_identities, [identity1]) + |> Ecto.Multi.put(:identities, [identity2]) + |> Ecto.Multi.put(:groups, [group1]) + |> Ecto.Multi.put(:upsert_groups, [group2]) + |> sync_provider_memberships_multi(provider, tuples_list) + + assert {:ok, + %{ + plan_memberships: {[], delete}, + delete_memberships: {2, nil}, + upsert_memberships: [] + }} = Repo.transaction(multi) + + assert {group1.id, identity1.actor_id} in delete + assert {group2.id, identity2.actor_id} in delete + + assert Repo.aggregate(Actors.Membership, :count) == 0 + assert Repo.aggregate(Actors.Membership.Query.all(), :count) == 0 + end + + test "ignores memberships that are not synced from the provider", %{ + account: account, + provider: provider, + group1: group1, + group2: group2, + identity1: identity1, + identity2: identity2 + } do + Fixtures.Actors.create_membership(account: account) + + tuples_list = [] + + multi = + Ecto.Multi.new() + |> Ecto.Multi.put(:insert_identities, [identity1]) + |> Ecto.Multi.put(:identities, [identity2]) + |> Ecto.Multi.put(:groups, [group1]) + |> Ecto.Multi.put(:upsert_groups, [group2]) + |> sync_provider_memberships_multi(provider, tuples_list) + + assert {:ok, + %{ + plan_memberships: {[], []}, + delete_memberships: {0, nil}, + upsert_memberships: [] + }} = Repo.transaction(multi) + end + end + describe "new_group/0" do test "returns group changeset" do assert %Ecto.Changeset{data: %Actors.Group{}, changes: changes} = new_group() @@ -141,81 +682,26 @@ defmodule Domain.ActorsTest do end end - describe "upsert_provider_group/3" do - setup do - account = AccountsFixtures.create_account() - - {provider, bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) - - %{ - bypass: bypass, - account: account, - provider: provider - } - end - - # test "creates a new group", %{provider: provider} do - # provider_identifier = Ecto.UUID.generate() - # attrs_by_provider_identifier = %{provider_identifier => %{name: "foo"}} - - # assert {:ok, group} = upsert_provider_group(provider, attrs) - - # assert group.provider_identifier == provider_identifier - # assert group.name == attrs.name - - # assert group.provider_id == provider.id - # assert group.account_id == provider.account_id - # refute group.deleted_at - - # assert Repo.one(Actors.Group) - # end - - # test "updates an existing group", %{account: account, provider: provider} do - # group = ActorsFixtures.create_provider_group(account: account, provider: provider) - - # provider_identifier = Ecto.UUID.generate() - # attrs = %{name: "foo"} - - # assert {:ok, updated_group} = upsert_provider_group_and_actors(provider, group.provider_identifier, attrs) - - # assert updated_group.provider_identifier == provider_identifier - # assert updated_group.name == group.name - # assert updated_group.name != attrs.name - - # assert updated_group.provider_id == provider.id - # assert updated_group.account_id == provider.account_id - # refute group.deleted_at - - # assert Repo.one(Actors.Group) - # end - - # test "deletes existing groups that are not synced" - - # updates membmers (removes old and adds new) - end - describe "group_synced?/1" do test "returns true for synced groups" do - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_userpass_provider(account: account) - group = ActorsFixtures.create_group(account: account, provider: provider) + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_userpass_provider(account: account) + group = Fixtures.Actors.create_group(account: account, provider: provider) assert group_synced?(group) end test "returns false for manually created groups" do - group = ActorsFixtures.create_group() + group = Fixtures.Actors.create_group() assert group_synced?(group) == false end end describe "create_group/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -235,14 +721,14 @@ defmodule Domain.ActorsTest do assert {:error, changeset} = create_group(attrs, subject) assert errors_on(changeset) == %{name: ["should be at most 64 character(s)"]} - ActorsFixtures.create_group(account: account, name: "foo") + Fixtures.Actors.create_group(account: account, name: "foo") attrs = %{name: "foo", tokens: [%{}]} assert {:error, changeset} = create_group(attrs, subject) assert "has already been taken" in errors_on(changeset).name end test "creates a group", %{subject: subject} do - attrs = ActorsFixtures.group_attrs() + attrs = Fixtures.Actors.group_attrs() assert {:ok, group} = create_group(attrs, subject) assert group.id @@ -254,7 +740,7 @@ defmodule Domain.ActorsTest do test "creates a group with memberships", %{account: account, actor: actor, subject: subject} do attrs = - ActorsFixtures.group_attrs( + Fixtures.Actors.group_attrs( memberships: [ %{actor_id: actor.id} ] @@ -274,7 +760,7 @@ defmodule Domain.ActorsTest do test "returns error when subject has no permission to manage groups", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert create_group(%{}, subject) == {:error, @@ -285,12 +771,12 @@ defmodule Domain.ActorsTest do describe "change_group/1" do test "returns changeset with given changes" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - group = ActorsFixtures.create_group(account: account) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + group = Fixtures.Actors.create_group(account: account) group_attrs = - ActorsFixtures.group_attrs( + Fixtures.Actors.group_attrs( memberships: [ %{actor_id: actor.id} ] @@ -306,9 +792,9 @@ defmodule Domain.ActorsTest do end test "raises if group is synced" do - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_userpass_provider(account: account) - group = ActorsFixtures.create_group(account: account, provider: provider) + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_userpass_provider(account: account) + group = Fixtures.Actors.create_group(account: account, provider: provider) assert_raise ArgumentError, "can't change synced groups", fn -> change_group(group, %{}) @@ -318,10 +804,10 @@ defmodule Domain.ActorsTest do describe "update_group/3" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -334,7 +820,7 @@ defmodule Domain.ActorsTest do test "does not allow to reset required fields to empty values", %{ subject: subject } do - group = ActorsFixtures.create_group() + group = Fixtures.Actors.create_group() attrs = %{name: nil} assert {:error, changeset} = update_group(group, attrs, subject) @@ -343,33 +829,33 @@ defmodule Domain.ActorsTest do end test "returns error on invalid attrs", %{account: account, subject: subject} do - group = ActorsFixtures.create_group(account: account) + group = Fixtures.Actors.create_group(account: account) attrs = %{name: String.duplicate("A", 65)} assert {:error, changeset} = update_group(group, attrs, subject) assert errors_on(changeset) == %{name: ["should be at most 64 character(s)"]} - ActorsFixtures.create_group(account: account, name: "foo") + Fixtures.Actors.create_group(account: account, name: "foo") attrs = %{name: "foo"} assert {:error, changeset} = update_group(group, attrs, subject) assert "has already been taken" in errors_on(changeset).name end test "updates a group", %{account: account, subject: subject} do - group = ActorsFixtures.create_group(account: account) + group = Fixtures.Actors.create_group(account: account) - attrs = ActorsFixtures.group_attrs() + attrs = Fixtures.Actors.group_attrs() assert {:ok, group} = update_group(group, attrs, subject) assert group.name == attrs.name end test "updates group memberships", %{account: account, actor: actor, subject: subject} do - group = ActorsFixtures.create_group(account: account, memberships: [%{actor_id: actor.id}]) + group = Fixtures.Actors.create_group(account: account, memberships: [%{actor_id: actor.id}]) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) attrs = - ActorsFixtures.group_attrs( + Fixtures.Actors.group_attrs( memberships: [ %{actor_id: other_actor.id} ] @@ -390,9 +876,9 @@ defmodule Domain.ActorsTest do account: account, subject: subject } do - group = ActorsFixtures.create_group(account: account) + group = Fixtures.Actors.create_group(account: account) - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert update_group(group, %{}, subject) == {:error, @@ -404,8 +890,8 @@ defmodule Domain.ActorsTest do account: account, subject: subject } do - provider = AuthFixtures.create_userpass_provider(account: account) - group = ActorsFixtures.create_group(account: account, provider: provider) + provider = Fixtures.Auth.create_userpass_provider(account: account) + group = Fixtures.Actors.create_group(account: account, provider: provider) assert update_group(group, %{}, subject) == {:error, :synced_group} end @@ -413,10 +899,10 @@ defmodule Domain.ActorsTest do describe "delete_group/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -427,7 +913,7 @@ defmodule Domain.ActorsTest do end test "returns error on state conflict", %{account: account, subject: subject} do - group = ActorsFixtures.create_group(account: account) + group = Fixtures.Actors.create_group(account: account) assert {:ok, deleted} = delete_group(group, subject) assert delete_group(deleted, subject) == {:error, :not_found} @@ -435,7 +921,7 @@ defmodule Domain.ActorsTest do end test "deletes groups", %{account: account, subject: subject} do - group = ActorsFixtures.create_group(account: account) + group = Fixtures.Actors.create_group(account: account) assert {:ok, deleted} = delete_group(group, subject) assert deleted.deleted_at @@ -444,9 +930,9 @@ defmodule Domain.ActorsTest do test "returns error when subject has no permission to delete groups", %{ subject: subject } do - group = ActorsFixtures.create_group() + group = Fixtures.Actors.create_group() - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert delete_group(group, subject) == {:error, @@ -458,19 +944,19 @@ defmodule Domain.ActorsTest do account: account, subject: subject } do - provider = AuthFixtures.create_userpass_provider(account: account) - group = ActorsFixtures.create_group(account: account, provider: provider) + provider = Fixtures.Auth.create_userpass_provider(account: account) + group = Fixtures.Actors.create_group(account: account, provider: provider) assert delete_group(group, subject) == {:error, :synced_group} end end - describe "fetch_count_by_type/0" do + describe "fetch_actors_count_by_type/0" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -483,33 +969,33 @@ defmodule Domain.ActorsTest do account: account, subject: subject } do - assert fetch_count_by_type(:account_admin_user, subject) == 1 - assert fetch_count_by_type(:account_user, subject) == 0 + assert fetch_actors_count_by_type(:account_admin_user, subject) == 1 + assert fetch_actors_count_by_type(:account_user, subject) == 0 - ActorsFixtures.create_actor(type: :account_admin_user) - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) + Fixtures.Actors.create_actor(type: :account_admin_user) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) assert {:ok, _actor} = delete_actor(actor, subject) - assert fetch_count_by_type(:account_admin_user, subject) == 1 - assert fetch_count_by_type(:account_user, subject) == 0 + assert fetch_actors_count_by_type(:account_admin_user, subject) == 1 + assert fetch_actors_count_by_type(:account_user, subject) == 0 - ActorsFixtures.create_actor(type: :account_admin_user, account: account) - assert fetch_count_by_type(:account_admin_user, subject) == 2 - assert fetch_count_by_type(:account_user, subject) == 0 + Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + assert fetch_actors_count_by_type(:account_admin_user, subject) == 2 + assert fetch_actors_count_by_type(:account_user, subject) == 0 - ActorsFixtures.create_actor(type: :account_user) - ActorsFixtures.create_actor(type: :account_user, account: account) - assert fetch_count_by_type(:account_admin_user, subject) == 2 - assert fetch_count_by_type(:account_user, subject) == 1 + Fixtures.Actors.create_actor(type: :account_user) + Fixtures.Actors.create_actor(type: :account_user, account: account) + assert fetch_actors_count_by_type(:account_admin_user, subject) == 2 + assert fetch_actors_count_by_type(:account_user, subject) == 1 - for _ <- 1..5, do: ActorsFixtures.create_actor(type: :account_user, account: account) - assert fetch_count_by_type(:account_admin_user, subject) == 2 - assert fetch_count_by_type(:account_user, subject) == 6 + for _ <- 1..5, do: Fixtures.Actors.create_actor(type: :account_user, account: account) + assert fetch_actors_count_by_type(:account_admin_user, subject) == 2 + assert fetch_actors_count_by_type(:account_user, subject) == 6 end test "returns error when subject can not view actors", %{subject: subject} do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) - assert fetch_count_by_type(:foo, subject) == + assert fetch_actors_count_by_type(:foo, subject) == {:error, {:unauthorized, [missing_permissions: [Actors.Authorizer.manage_actors_permission()]]}} @@ -518,44 +1004,42 @@ defmodule Domain.ActorsTest do describe "fetch_groups_count_grouped_by_provider_id/1" do test "returns empty map when there are no groups" do - subject = AuthFixtures.create_subject() + subject = Fixtures.Auth.create_subject() assert fetch_groups_count_grouped_by_provider_id(subject) == {:ok, %{}} end test "returns count of actor groups by provider id" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {google_provider, _bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account, name: "google") {vault_provider, _bypass} = - AuthFixtures.start_openid_providers(["vault"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account, name: "vault") - ActorsFixtures.create_group( + Fixtures.Actors.create_group( account: account, subject: subject ) - ActorsFixtures.create_group( + Fixtures.Actors.create_group( account: account, subject: subject, provider: google_provider, provider_identifier: Ecto.UUID.generate() ) - ActorsFixtures.create_group( + Fixtures.Actors.create_group( account: account, subject: subject, provider: vault_provider, provider_identifier: Ecto.UUID.generate() ) - ActorsFixtures.create_group( + Fixtures.Actors.create_group( account: account, subject: subject, provider: vault_provider, @@ -573,51 +1057,51 @@ defmodule Domain.ActorsTest do describe "fetch_actor_by_id/2" do test "returns error when actor is not found" do - subject = AuthFixtures.create_subject() + subject = Fixtures.Auth.create_subject() assert fetch_actor_by_id(Ecto.UUID.generate(), subject) == {:error, :not_found} end test "returns error when id is not a valid UUID" do - subject = AuthFixtures.create_subject() + subject = Fixtures.Auth.create_subject() assert fetch_actor_by_id("foo", subject) == {:error, :not_found} end test "returns own actor" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert {:ok, returned_actor} = fetch_actor_by_id(actor.id, subject) assert returned_actor.id == actor.id end test "returns non own actor" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) - actor = ActorsFixtures.create_actor(account: account) + actor = Fixtures.Actors.create_actor(account: account) assert {:ok, returned_actor} = fetch_actor_by_id(actor.id, subject) assert returned_actor.id == actor.id end test "returns error when actor is in another account" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) - actor = ActorsFixtures.create_actor() + actor = Fixtures.Actors.create_actor() assert fetch_actor_by_id(actor.id, subject) == {:error, :not_found} end test "returns error when subject can not view actors" do - subject = AuthFixtures.create_subject() - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.create_subject() + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_actor_by_id("foo", subject) == {:error, @@ -626,10 +1110,10 @@ defmodule Domain.ActorsTest do end test "associations are preloaded when opts given" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, actor} = fetch_actor_by_id(actor.id, subject, preload: :identities) @@ -647,7 +1131,7 @@ defmodule Domain.ActorsTest do end test "returns actor" do - actor = ActorsFixtures.create_actor(type: :account_admin_user) + actor = Fixtures.Actors.create_actor(type: :account_admin_user) assert {:ok, returned_actor} = fetch_actor_by_id(actor.id) assert returned_actor.id == actor.id end @@ -667,7 +1151,7 @@ defmodule Domain.ActorsTest do end test "returns actor" do - actor = ActorsFixtures.create_actor(type: :account_admin_user) + actor = Fixtures.Actors.create_actor(type: :account_admin_user) assert returned_actor = fetch_actor_by_id!(actor.id) assert returned_actor.id == actor.id end @@ -684,22 +1168,21 @@ defmodule Domain.ActorsTest do expires_at: nil, permissions: MapSet.new() } - |> AuthFixtures.set_permissions([ + |> Fixtures.Auth.set_permissions([ Actors.Authorizer.manage_actors_permission() ]) assert list_actors(subject) == {:ok, []} - assert list_actors(subject, hydrate: []) == {:ok, []} end test "returns list of actors in all types" do - account = AccountsFixtures.create_account() - actor1 = ActorsFixtures.create_actor(account: account, type: :account_admin_user) - actor2 = ActorsFixtures.create_actor(account: account, type: :account_user) - ActorsFixtures.create_actor(type: :account_user) + account = Fixtures.Accounts.create_account() + actor1 = Fixtures.Actors.create_actor(account: account, type: :account_admin_user) + actor2 = Fixtures.Actors.create_actor(account: account, type: :account_user) + Fixtures.Actors.create_actor(type: :account_user) - identity1 = AuthFixtures.create_identity(account: account, actor: actor1) - subject = AuthFixtures.create_subject(identity1) + identity1 = Fixtures.Auth.create_identity(account: account, actor: actor1) + subject = Fixtures.Auth.create_subject(identity: identity1) assert {:ok, actors} = list_actors(subject) assert length(actors) == 2 @@ -707,8 +1190,8 @@ defmodule Domain.ActorsTest do end test "returns error when subject can not view actors" do - subject = AuthFixtures.create_subject() - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.create_subject() + subject = Fixtures.Auth.remove_permissions(subject) assert list_actors(subject) == {:error, @@ -717,14 +1200,14 @@ defmodule Domain.ActorsTest do end test "associations are preloaded when opts given" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() - actor1 = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity1 = AuthFixtures.create_identity(account: account, actor: actor1) - subject = AuthFixtures.create_subject(identity1) + actor1 = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity1 = Fixtures.Auth.create_identity(account: account, actor: actor1) + subject = Fixtures.Auth.create_subject(identity: identity1) - actor2 = ActorsFixtures.create_actor(type: :account_user, account: account) - AuthFixtures.create_identity(account: account, actor: actor2) + actor2 = Fixtures.Actors.create_actor(type: :account_user, account: account) + Fixtures.Auth.create_identity(account: account, actor: actor2) {:ok, actors} = list_actors(subject, preload: :identities) assert length(actors) == 2 @@ -735,24 +1218,17 @@ defmodule Domain.ActorsTest do describe "create_actor/4" do setup do - Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_email_provider(account: account) - provider_identifier = AuthFixtures.random_provider_identifier(provider) + account = Fixtures.Accounts.create_account() %{ - account: account, - provider: provider, - provider_identifier: provider_identifier + account: account } end test "returns changeset error when required attrs are missing", %{ - provider: provider, - provider_identifier: provider_identifier + account: account } do - assert {:error, changeset} = create_actor(provider, provider_identifier, %{}) + assert {:error, changeset} = create_actor(account, %{}) refute changeset.valid? assert errors_on(changeset) == %{ @@ -762,12 +1238,11 @@ defmodule Domain.ActorsTest do end test "returns error on invalid attrs", %{ - provider: provider, - provider_identifier: provider_identifier + account: account } do - attrs = ActorsFixtures.actor_attrs(type: :foo) + attrs = Fixtures.Actors.actor_attrs(type: :foo) - assert {:error, changeset} = create_actor(provider, provider_identifier, attrs) + assert {:error, changeset} = create_actor(account, attrs) refute changeset.valid? assert errors_on(changeset) == %{ @@ -775,137 +1250,95 @@ defmodule Domain.ActorsTest do } end - test "upserts the identity based on unique provider_identifier", %{ - provider: provider - } do - provider_identifier = AuthFixtures.random_provider_identifier(provider) - attrs = ActorsFixtures.actor_attrs() - assert {:ok, _actor} = create_actor(provider, provider_identifier, attrs) - assert {:error, changeset} = create_actor(provider, provider_identifier, attrs) - assert errors_on(changeset) == %{provider_identifier: ["has already been taken"]} - end - test "creates an actor in given type", %{ - provider: provider + account: account } do for type <- [:account_user, :account_admin_user, :service_account] do - attrs = ActorsFixtures.actor_attrs(type: type) - provider_identifier = AuthFixtures.random_provider_identifier(provider) - assert {:ok, actor} = create_actor(provider, provider_identifier, attrs) + attrs = Fixtures.Actors.actor_attrs(type: type) + assert {:ok, actor} = create_actor(account, attrs) assert actor.type == type end end - test "creates an actor and identity", %{ - provider: provider, - provider_identifier: provider_identifier + test "creates an actor", %{ + account: account } do - attrs = ActorsFixtures.actor_attrs() + attrs = Fixtures.Actors.actor_attrs() - assert {:ok, actor} = create_actor(provider, provider_identifier, attrs) + assert {:ok, actor} = create_actor(account, attrs) assert actor.type == attrs.type assert actor.type == attrs.type assert is_nil(actor.disabled_at) assert is_nil(actor.deleted_at) - - assert identity = Repo.one(Domain.Auth.Identity) - assert identity.provider_id == provider.id - assert identity.provider_identifier == provider_identifier - assert identity.actor_id == actor.id - assert identity.account_id == provider.account_id - - assert %{"sign_in_token_created_at" => _, "sign_in_token_hash" => _} = - identity.provider_state - - assert identity.provider_virtual_state == nil - - assert is_nil(identity.deleted_at) end end describe "create_actor/5" do setup do - Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_email_provider(account: account) - provider_identifier = AuthFixtures.random_provider_identifier(provider) + account = Fixtures.Accounts.create_account() %{ - account: account, - provider: provider, - provider_identifier: provider_identifier + account: account } end test "returns error when subject can not create actors", %{ - account: account, - provider: provider, - provider_identifier: provider_identifier + account: account } do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) subject = - AuthFixtures.create_identity(account: account, actor: actor) - |> AuthFixtures.create_subject() - |> AuthFixtures.remove_permissions() + Fixtures.Auth.create_subject(account: account, identity: [actor: actor]) + |> Fixtures.Auth.remove_permissions() - assert create_actor(provider, provider_identifier, %{}, subject) == + attrs = %{} + + assert create_actor(account, attrs, subject) == {:error, {:unauthorized, [missing_permissions: [Actors.Authorizer.manage_actors_permission()]]}} end test "returns error when subject tries to create an account in another account", %{ - provider: provider, - provider_identifier: provider_identifier + account: account } do - subject = AuthFixtures.create_subject() - assert create_actor(provider, provider_identifier, %{}, subject) == {:error, :unauthorized} + subject = Fixtures.Auth.create_subject() + attrs = %{} + assert create_actor(account, attrs, subject) == {:error, :unauthorized} end test "returns error when subject is trying to create an actor with a privilege escalation", %{ - account: account, - provider: provider, - provider_identifier: provider_identifier + account: account } do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) - subject = - AuthFixtures.create_identity(account: account, actor: actor) - |> AuthFixtures.create_subject() + subject = Fixtures.Auth.create_subject(account: account, identity: [actor: actor]) - admin_permissions = subject.permissions required_permissions = [Actors.Authorizer.manage_actors_permission()] subject = subject - |> AuthFixtures.remove_permissions() - |> AuthFixtures.set_permissions(required_permissions) + |> Fixtures.Auth.remove_permissions() + |> Fixtures.Auth.set_permissions(required_permissions) - missing_permissions = - MapSet.difference(admin_permissions, MapSet.new(required_permissions)) - |> MapSet.to_list() + attrs = %{ + type: :account_admin_user, + name: "John Smith" + } - attrs = %{type: :account_admin_user, name: "John Smith"} + assert {:error, changeset} = create_actor(account, attrs, subject) - assert create_actor(provider, provider_identifier, attrs, subject) == - {:error, {:unauthorized, privilege_escalation: missing_permissions}} - - attrs = %{"type" => "account_admin_user", "name" => "John Smith"} - - assert create_actor(provider, provider_identifier, attrs, subject) == - {:error, {:unauthorized, privilege_escalation: missing_permissions}} + assert "does not have permissions to grant this actor type" in errors_on(changeset).type end end - describe "change_actor_type/3" do + describe "update_actor/3" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -914,29 +1347,57 @@ defmodule Domain.ActorsTest do } end + test "allows changing name of an actor", %{account: account, subject: subject} do + actor = Fixtures.Actors.create_actor(name: "ABC", account: account) + assert {:ok, %{name: "DEF"}} = update_actor(actor, %{name: "DEF"}, subject) + assert {:ok, %{name: "ABC"}} = update_actor(actor, %{name: "ABC"}, subject) + end + + test "does not allow changing name of a synced actor", %{account: account, subject: subject} do + actor = + Fixtures.Actors.create_actor(name: "ABC", account: account) + |> Fixtures.Actors.update(last_synced_at: DateTime.utc_now()) + + assert {:ok, %{name: "ABC"}} = update_actor(actor, %{name: "DEF"}, subject) + end + test "allows admin to change other actors type", %{account: account, subject: subject} do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - assert {:ok, %{type: :account_user}} = change_actor_type(actor, :account_user, subject) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + assert {:ok, %{type: :account_user}} = update_actor(actor, %{type: :account_user}, subject) assert {:ok, %{type: :account_admin_user}} = - change_actor_type(actor, :account_admin_user, subject) + update_actor(actor, %{type: :account_admin_user}, subject) - actor = ActorsFixtures.create_actor(type: :account_user, account: account) - assert {:ok, %{type: :account_user}} = change_actor_type(actor, :account_user, subject) + actor = Fixtures.Actors.create_actor(type: :account_user, account: account) + assert {:ok, %{type: :account_user}} = update_actor(actor, %{type: :account_user}, subject) assert {:ok, %{type: :account_admin_user}} = - change_actor_type(actor, :account_admin_user, subject) + update_actor(actor, %{type: :account_admin_user}, subject) + end + + test "allows admin to change synced actors type", %{account: account, subject: subject} do + actor = + Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + |> Fixtures.Actors.update(last_synced_at: DateTime.utc_now()) + + assert {:ok, %{type: :account_user}} = update_actor(actor, %{type: :account_user}, subject) + + actor = + Fixtures.Actors.create_actor(type: :account_user, account: account) + |> Fixtures.Actors.update(last_synced_at: DateTime.utc_now()) + + assert {:ok, %{type: :account_admin_user}} = + update_actor(actor, %{type: :account_admin_user}, subject) end test "returns error when subject can not manage types", %{account: account} do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) subject = - AuthFixtures.create_identity(account: account, actor: actor) - |> AuthFixtures.create_subject() - |> AuthFixtures.remove_permissions() + Fixtures.Auth.create_subject(account: account, identity: [actor: actor]) + |> Fixtures.Auth.remove_permissions() - assert change_actor_type(actor, :foo, subject) == + assert update_actor(actor, %{type: :foo}, subject) == {:error, {:unauthorized, [missing_permissions: [Actors.Authorizer.manage_actors_permission()]]}} @@ -945,11 +1406,11 @@ defmodule Domain.ActorsTest do describe "disable_actor/2" do test "disables a given actor" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert {:ok, actor} = disable_actor(actor, subject) assert actor.disabled_at @@ -962,30 +1423,30 @@ defmodule Domain.ActorsTest do end test "returns error when trying to disable the last admin actor" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(account: account, type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert disable_actor(actor, subject) == {:error, :cant_disable_the_last_admin} end test "last admin check ignores admins in other accounts" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - ActorsFixtures.create_actor(type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + Fixtures.Actors.create_actor(type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert disable_actor(actor, subject) == {:error, :cant_disable_the_last_admin} end test "last admin check ignores disabled admins" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _other_actor} = disable_actor(other_actor, subject) assert disable_actor(actor, subject) == {:error, :cant_disable_the_last_admin} @@ -1000,39 +1461,39 @@ defmodule Domain.ActorsTest do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_email_provider(account: account) + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_email_provider(account: account) actor_one = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider ) actor_two = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider ) identity_one = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, actor: actor_one, provider: provider ) identity_two = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, actor: actor_two, provider: provider ) - subject_one = AuthFixtures.create_subject(identity_one) - subject_two = AuthFixtures.create_subject(identity_two) + subject_one = Fixtures.Auth.create_subject(identity: identity_one) + subject_two = Fixtures.Auth.create_subject(identity: identity_two) for {actor, subject} <- [{actor_two, subject_one}, {actor_one, subject_two}] do Task.async(fn -> @@ -1053,11 +1514,11 @@ defmodule Domain.ActorsTest do end test "does not do anything when an actor is disabled twice" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert {:ok, _actor} = disable_actor(other_actor, subject) assert {:ok, other_actor} = disable_actor(other_actor, subject) @@ -1065,23 +1526,22 @@ defmodule Domain.ActorsTest do end test "does not allow to disable actors in other accounts" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert disable_actor(other_actor, subject) == {:error, :not_found} end test "returns error when subject can not disable actors" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) subject = - AuthFixtures.create_identity(account: account, actor: actor) - |> AuthFixtures.create_subject() - |> AuthFixtures.remove_permissions() + Fixtures.Auth.create_subject(account: account, identity: [actor: actor]) + |> Fixtures.Auth.remove_permissions() assert disable_actor(actor, subject) == {:error, @@ -1092,30 +1552,30 @@ defmodule Domain.ActorsTest do describe "enable_actor/2" do test "enables a given actor" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, actor} = disable_actor(actor, subject) assert {:ok, actor} = enable_actor(actor, subject) - assert actor.disabled_at + refute actor.disabled_at assert actor = Repo.get(Actors.Actor, actor.id) - assert actor.disabled_at + refute actor.disabled_at assert other_actor = Repo.get(Actors.Actor, other_actor.id) assert is_nil(other_actor.disabled_at) end test "does not do anything when an actor is already enabled" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, other_actor} = disable_actor(other_actor, subject) @@ -1125,23 +1585,22 @@ defmodule Domain.ActorsTest do end test "does not allow to enable actors in other accounts" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert enable_actor(other_actor, subject) == {:error, :not_found} end test "returns error when subject can not enable actors" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) subject = - AuthFixtures.create_identity(account: account, actor: actor) - |> AuthFixtures.create_subject() - |> AuthFixtures.remove_permissions() + Fixtures.Auth.create_subject(account: account, identity: [actor: actor]) + |> Fixtures.Auth.remove_permissions() assert enable_actor(actor, subject) == {:error, @@ -1152,11 +1611,11 @@ defmodule Domain.ActorsTest do describe "delete_actor/2" do test "deletes a given actor" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert {:ok, actor} = delete_actor(actor, subject) assert actor.deleted_at @@ -1168,36 +1627,65 @@ defmodule Domain.ActorsTest do assert is_nil(other_actor.deleted_at) end + test "deletes actor identities and devices" do + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) + + actor_to_delete = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + Fixtures.Auth.create_identity(account: account, actor: actor_to_delete) + Fixtures.Devices.create_device(account: account, actor: actor_to_delete) + + assert {:ok, actor} = delete_actor(actor_to_delete, subject) + assert actor.deleted_at + + assert Repo.aggregate(Domain.Devices.Device.Query.all(), :count) == 0 + assert Repo.aggregate(Domain.Auth.Identity.Query.all(), :count) == 1 + end + test "returns error when trying to delete the last admin actor" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert delete_actor(actor, subject) == {:error, :cant_delete_the_last_admin} end test "last admin check ignores admins in other accounts" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - ActorsFixtures.create_actor(type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + Fixtures.Actors.create_actor(type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert delete_actor(actor, subject) == {:error, :cant_delete_the_last_admin} end test "last admin check ignores disabled admins" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _other_actor} = disable_actor(other_actor, subject) assert delete_actor(actor, subject) == {:error, :cant_delete_the_last_admin} end + test "last admin check ignores service accounts" do + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) + + actor = Fixtures.Actors.create_actor(type: :service_account, account: account) + + assert {:ok, actor} = delete_actor(actor, subject) + assert actor.deleted_at + end + test "returns error when trying to delete the last admin actor using a race condition" do for _ <- 0..50 do test_pid = self() @@ -1207,39 +1695,39 @@ defmodule Domain.ActorsTest do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_email_provider(account: account) + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_email_provider(account: account) actor_one = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider ) actor_two = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider ) identity_one = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, actor: actor_one, provider: provider ) identity_two = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, actor: actor_two, provider: provider ) - subject_one = AuthFixtures.create_subject(identity_one) - subject_two = AuthFixtures.create_subject(identity_two) + subject_one = Fixtures.Auth.create_subject(identity: identity_one) + subject_two = Fixtures.Auth.create_subject(identity: identity_two) for {actor, subject} <- [{actor_two, subject_one}, {actor_one, subject_two}] do Task.async(fn -> @@ -1256,34 +1744,33 @@ defmodule Domain.ActorsTest do end test "does not allow to delete an actor twice" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert {:ok, _actor} = delete_actor(other_actor, subject) assert delete_actor(other_actor, subject) == {:error, :not_found} end test "does not allow to delete actors in other accounts" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - other_actor = ActorsFixtures.create_actor(type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + other_actor = Fixtures.Actors.create_actor(type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert delete_actor(other_actor, subject) == {:error, :not_found} end test "returns error when subject can not delete actors" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) subject = - AuthFixtures.create_identity(account: account, actor: actor) - |> AuthFixtures.create_subject() - |> AuthFixtures.remove_permissions() + Fixtures.Auth.create_subject(account: account, identity: [actor: actor]) + |> Fixtures.Auth.remove_permissions() assert delete_actor(actor, subject) == {:error, diff --git a/elixir/apps/domain/test/domain/auth/adapters/email_test.exs b/elixir/apps/domain/test/domain/auth/adapters/email_test.exs index 050b55ceb..7ad87e490 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/email_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/email_test.exs @@ -2,14 +2,13 @@ defmodule Domain.Auth.Adapters.EmailTest do use Domain.DataCase, async: true import Domain.Auth.Adapters.Email alias Domain.Auth - alias Domain.{AccountsFixtures, AuthFixtures} describe "identity_changeset/2" do setup do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_email_provider(account: account) + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_email_provider(account: account) changeset = %Auth.Identity{} |> Ecto.Changeset.change() %{ @@ -44,13 +43,13 @@ defmodule Domain.Auth.Adapters.EmailTest do test "returns changeset as is" do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() changeset = %Ecto.Changeset{data: %Domain.Auth.Provider{account_id: account.id}} assert provider_changeset(changeset) == changeset end test "returns error when email adapter is not configured" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() changeset = %Ecto.Changeset{data: %Domain.Auth.Provider{account_id: account.id}} changeset = provider_changeset(changeset) assert changeset.errors == [adapter: {"email adapter is not configured", []}] @@ -60,7 +59,7 @@ defmodule Domain.Auth.Adapters.EmailTest do describe "ensure_provisioned/1" do test "does nothing for a provider" do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider() + provider = Fixtures.Auth.create_email_provider() assert ensure_provisioned(provider) == {:ok, provider} end end @@ -68,14 +67,14 @@ defmodule Domain.Auth.Adapters.EmailTest do describe "ensure_deprovisioned/1" do test "does nothing for a provider" do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider() + provider = Fixtures.Auth.create_email_provider() assert ensure_deprovisioned(provider) == {:ok, provider} end end describe "request_sign_in_token/1" do test "returns identity with updated sign-in token" do - identity = AuthFixtures.create_identity() + identity = Fixtures.Auth.create_identity() assert {:ok, identity} = request_sign_in_token(identity) @@ -97,9 +96,9 @@ defmodule Domain.Auth.Adapters.EmailTest do setup do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_email_provider(account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider) + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_email_provider(account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) token = identity.provider_virtual_state.sign_in_token %{account: account, provider: provider, identity: identity, token: token} @@ -122,7 +121,7 @@ defmodule Domain.Auth.Adapters.EmailTest do forty_seconds_ago = DateTime.utc_now() |> DateTime.add(-1 * 15 * 60 - 1, :second) identity = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, provider: provider, provider_state: %{ diff --git a/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs_test.exs b/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs_test.exs new file mode 100644 index 000000000..c734a879e --- /dev/null +++ b/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs_test.exs @@ -0,0 +1,253 @@ +defmodule Domain.Auth.Adapters.GoogleWorkspace.JobsTest do + use Domain.DataCase, async: true + alias Domain.{Auth, Actors} + alias Domain.Mocks.GoogleWorkspaceDirectory + import Domain.Auth.Adapters.GoogleWorkspace.Jobs + + describe "refresh_access_tokens/1" do + setup do + account = Fixtures.Accounts.create_account() + + {provider, bypass} = + Fixtures.Auth.start_and_create_google_workspace_provider(account: account) + + provider = + Domain.Fixture.update!(provider, %{ + adapter_state: %{ + "access_token" => "OIDC_ACCESS_TOKEN", + "refresh_token" => "OIDC_REFRESH_TOKEN", + "expires_at" => DateTime.utc_now() |> DateTime.add(15, :minute), + "claims" => "openid email profile offline_access" + } + }) + + identity = Fixtures.Auth.create_identity(account: account, provider: provider) + + %{ + bypass: bypass, + account: account, + provider: provider, + identity: identity + } + end + + test "refreshes the access token", %{ + provider: provider, + identity: identity, + bypass: bypass + } do + {token, claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) + + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{ + "token_type" => "Bearer", + "id_token" => token, + "access_token" => "MY_ACCESS_TOKEN", + "refresh_token" => "MY_REFRESH_TOKEN", + "expires_in" => nil + }) + + Mocks.OpenIDConnect.expect_userinfo(bypass) + + assert refresh_access_tokens(%{}) == :ok + + provider = Repo.get!(Domain.Auth.Provider, provider.id) + + assert %{ + "access_token" => "MY_ACCESS_TOKEN", + "claims" => ^claims, + "expires_at" => expires_at, + "refresh_token" => "MY_REFRESH_TOKEN", + "userinfo" => %{ + "email" => "ada@example.com", + "email_verified" => true, + "family_name" => "Lovelace", + "given_name" => "Ada", + "locale" => "en", + "name" => "Ada Lovelace", + "picture" => + "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg", + "sub" => "353690423699814251281" + } + } = provider.adapter_state + + assert expires_at + end + + test "does not crash when endpoint it not available", %{ + bypass: bypass + } do + Bypass.down(bypass) + assert refresh_access_tokens(%{}) == :ok + end + end + + describe "sync_directory/1" do + setup do + account = Fixtures.Accounts.create_account() + + {provider, bypass} = + Fixtures.Auth.start_and_create_google_workspace_provider(account: account) + + %{ + bypass: bypass, + account: account, + provider: provider + } + end + + test "syncs IdP data", %{provider: provider} do + bypass = Bypass.open() + + groups = [ + %{ + "kind" => "admin#directory#group", + "id" => "GROUP_ID1", + "etag" => "\"ET\"", + "email" => "i@fiez.xxx", + "name" => "Infrastructure", + "directMembersCount" => "5", + "description" => "Group to handle infrastructure alerts and management", + "adminCreated" => true, + "aliases" => [ + "pnr@firez.one" + ], + "nonEditableAliases" => [ + "i@ext.fiez.xxx" + ] + } + ] + + organization_units = [ + %{ + "kind" => "admin#directory#orgUnit", + "name" => "Engineering", + "description" => "Engineering team", + "etag" => "\"ET\"", + "blockInheritance" => false, + "orgUnitId" => "OU_ID1", + "orgUnitPath" => "/Engineering", + "parentOrgUnitId" => "OU_ID0", + "parentOrgUnitPath" => "/" + } + ] + + users = [ + %{ + "agreedToTerms" => true, + "archived" => false, + "changePasswordAtNextLogin" => false, + "creationTime" => "2023-06-10T17:32:06.000Z", + "customerId" => "CustomerID1", + "emails" => [ + %{"address" => "b@firez.xxx", "primary" => true}, + %{"address" => "b@ext.firez.xxx"} + ], + "etag" => "\"ET-61Bnx4\"", + "id" => "USER_ID1", + "includeInGlobalAddressList" => true, + "ipWhitelisted" => false, + "isAdmin" => false, + "isDelegatedAdmin" => false, + "isEnforcedIn2Sv" => false, + "isEnrolledIn2Sv" => false, + "isMailboxSetup" => true, + "kind" => "admin#directory#user", + "languages" => [%{"languageCode" => "en", "preference" => "preferred"}], + "lastLoginTime" => "2023-06-26T13:53:30.000Z", + "name" => %{ + "familyName" => "Manifold", + "fullName" => "Brian Manifold", + "givenName" => "Brian" + }, + "nonEditableAliases" => ["b@ext.firez.xxx"], + "orgUnitPath" => "/Engineering", + "organizations" => [ + %{ + "customType" => "", + "department" => "Engineering", + "location" => "", + "name" => "Firezone, Inc.", + "primary" => true, + "title" => "Senior Fullstack Engineer", + "type" => "work" + } + ], + "phones" => [%{"type" => "mobile", "value" => "(567) 111-2233"}], + "primaryEmail" => "b@firez.xxx", + "recoveryEmail" => "xxx@xxx.com", + "suspended" => false, + "thumbnailPhotoEtag" => "\"ET\"", + "thumbnailPhotoUrl" => + "https://lh3.google.com/ao/AP2z2aWvm9JM99oCFZ1TVOJgQZlmZdMMYNr7w9G0jZApdTuLHfAueGFb_XzgTvCNRhGw=s96-c" + } + ] + + members = [ + %{ + "kind" => "admin#directory#member", + "etag" => "\"ET\"", + "id" => "USER_ID1", + "email" => "b@firez.xxx", + "role" => "MEMBER", + "type" => "USER", + "status" => "ACTIVE" + } + ] + + GoogleWorkspaceDirectory.override_endpoint_url("http://localhost:#{bypass.port}/") + GoogleWorkspaceDirectory.mock_groups_list_endpoint(bypass, groups) + GoogleWorkspaceDirectory.mock_organization_units_list_endpoint(bypass, organization_units) + GoogleWorkspaceDirectory.mock_users_list_endpoint(bypass, users) + + Enum.each(groups, fn group -> + GoogleWorkspaceDirectory.mock_group_members_list_endpoint(bypass, group["id"], members) + end) + + assert sync_directory(%{}) == :ok + + groups = Actors.Group |> Repo.all() + assert length(groups) == 2 + + for group <- groups do + assert group.provider_identifier in ["G:GROUP_ID1", "OU:OU_ID1"] + assert group.name in ["OrgUnit:Engineering", "Group:Infrastructure"] + + assert group.inserted_at + assert group.updated_at + + assert group.created_by == :provider + assert group.provider_id == provider.id + end + + identities = Auth.Identity |> Repo.all() |> Repo.preload(:actor) + assert length(identities) == 1 + + for identity <- identities do + assert identity.inserted_at + assert identity.created_by == :provider + assert identity.provider_id == provider.id + assert identity.provider_identifier in ["USER_ID1"] + assert identity.actor.name in ["Brian Manifold"] + assert identity.actor.last_synced_at + end + + memberships = Actors.Membership |> Repo.all() + assert length(memberships) == 2 + membership_tuples = Enum.map(memberships, &{&1.group_id, &1.actor_id}) + + for identity <- identities, group <- groups do + assert {group.id, identity.actor_id} in membership_tuples + end + end + + test "does not crash on endpoint errors" do + bypass = Bypass.open() + Bypass.down(bypass) + GoogleWorkspaceDirectory.override_endpoint_url("http://localhost:#{bypass.port}/") + + assert sync_directory(%{}) == :ok + + assert Repo.aggregate(Actors.Group, :count) == 0 + end + end +end diff --git a/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs b/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs index caab566a9..7802a5c1e 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs @@ -3,15 +3,13 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do import Domain.Auth.Adapters.GoogleWorkspace alias Domain.Auth alias Domain.Auth.Adapters.OpenIDConnect.PKCE - alias Domain.{AccountsFixtures, AuthFixtures} describe "identity_changeset/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() {provider, bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_google_workspace_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) changeset = %Auth.Identity{} |> Ecto.Changeset.change() @@ -41,7 +39,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do assert %Ecto.Changeset{} = changeset = provider_changeset(changeset) assert errors_on(changeset) == %{adapter_config: ["can't be blank"]} - attrs = AuthFixtures.provider_attrs(adapter: :google_workspace, adapter_config: %{}) + attrs = Fixtures.Auth.provider_attrs(adapter: :google_workspace, adapter_config: %{}) changeset = Ecto.Changeset.change(%Auth.Provider{}, attrs) assert %Ecto.Changeset{} = changeset = provider_changeset(changeset) @@ -54,16 +52,17 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do end test "returns changeset on valid adapter config" do - account = AccountsFixtures.create_account() - {_bypass, discovery_document_uri} = AuthFixtures.discovery_document_server() + account = Fixtures.Accounts.create_account() + bypass = Domain.Mocks.OpenIDConnect.discovery_document_server() + discovery_document_url = "http://localhost:#{bypass.port}/.well-known/openid-configuration" attrs = - AuthFixtures.provider_attrs( + Fixtures.Auth.provider_attrs( adapter: :google_workspace, adapter_config: %{ client_id: "client_id", client_secret: "client_secret", - discovery_document_uri: discovery_document_uri + discovery_document_uri: discovery_document_url } ) @@ -91,30 +90,26 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do "response_type" => "code", "client_id" => "client_id", "client_secret" => "client_secret", - "discovery_document_uri" => discovery_document_uri + "discovery_document_uri" => discovery_document_url } end end describe "ensure_deprovisioned/1" do test "does nothing for a provider" do - {provider, _bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_google_workspace_provider() - + {provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider() assert ensure_deprovisioned(provider) == {:ok, provider} end end describe "verify_and_update_identity/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() {provider, bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_google_workspace_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) %{account: account, provider: provider, identity: identity, bypass: bypass} end @@ -124,10 +119,10 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do identity: identity, bypass: bypass } do - {token, claims} = AuthFixtures.generate_openid_connect_token(provider, identity) + {token, claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -159,9 +154,9 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do identity: identity, bypass: bypass } do - {token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity) + {token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{ + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{ "token_type" => "Bearer", "id_token" => token, "access_token" => "MY_ACCESS_TOKEN", @@ -169,7 +164,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do "expires_in" => 3600 }) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -190,11 +185,11 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do forty_seconds_ago = DateTime.utc_now() |> DateTime.add(-40, :second) |> DateTime.to_unix() {token, _claims} = - AuthFixtures.generate_openid_connect_token(provider, identity, %{ + Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity, %{ "exp" => forty_seconds_ago }) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -209,7 +204,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do } do token = "foo" - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -224,9 +219,11 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do bypass: bypass } do {token, _claims} = - AuthFixtures.generate_openid_connect_token(provider, identity, %{"sub" => "foo@bar.com"}) + Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity, %{ + "sub" => "foo@bar.com" + }) - AuthFixtures.expect_refresh_token(bypass, %{ + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{ "token_type" => "Bearer", "id_token" => token, "access_token" => "MY_ACCESS_TOKEN", @@ -234,7 +231,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do "expires_in" => 3600 }) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -248,10 +245,10 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do provider: provider, bypass: bypass } do - identity = AuthFixtures.create_identity(account: account) - {token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity) + identity = Fixtures.Auth.create_identity(account: account) + {token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{ + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{ "token_type" => "Bearer", "id_token" => token, "access_token" => "MY_ACCESS_TOKEN", @@ -259,7 +256,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do "expires_in" => 3600 }) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" diff --git a/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs b/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs index a58471cd9..447c55e9e 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs @@ -3,15 +3,13 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do import Domain.Auth.Adapters.OpenIDConnect alias Domain.Auth alias Domain.Auth.Adapters.OpenIDConnect.{PKCE, State} - alias Domain.{AccountsFixtures, AuthFixtures} describe "identity_changeset/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() {provider, bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) changeset = %Auth.Identity{} |> Ecto.Changeset.change() @@ -37,12 +35,12 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do describe "provider_changeset/1" do test "returns changeset errors in invalid adapter config" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() changeset = Ecto.Changeset.change(%Auth.Provider{account_id: account.id}, %{}) assert %Ecto.Changeset{} = changeset = provider_changeset(changeset) assert errors_on(changeset) == %{adapter_config: ["can't be blank"]} - attrs = AuthFixtures.provider_attrs(adapter: :openid_connect, adapter_config: %{}) + attrs = Fixtures.Auth.provider_attrs(adapter: :openid_connect, adapter_config: %{}) changeset = Ecto.Changeset.change(%Auth.Provider{account_id: account.id}, attrs) assert %Ecto.Changeset{} = changeset = provider_changeset(changeset) @@ -56,16 +54,17 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do end test "returns changeset on valid adapter config" do - account = AccountsFixtures.create_account() - {_bypass, discovery_document_uri} = AuthFixtures.discovery_document_server() + account = Fixtures.Accounts.create_account() + bypass = Domain.Mocks.OpenIDConnect.discovery_document_server() + discovery_document_url = "http://localhost:#{bypass.port}/.well-known/openid-configuration" attrs = - AuthFixtures.provider_attrs( + Fixtures.Auth.provider_attrs( adapter: :openid_connect, adapter_config: %{ client_id: "client_id", client_secret: "client_secret", - discovery_document_uri: discovery_document_uri + discovery_document_uri: discovery_document_url } ) @@ -82,18 +81,17 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do "response_type" => "code", "client_id" => "client_id", "client_secret" => "client_secret", - "discovery_document_uri" => discovery_document_uri + "discovery_document_uri" => discovery_document_url } end end describe "ensure_provisioned/1" do test "does nothing for a provider" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() {provider, _bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) assert ensure_provisioned(provider) == {:ok, provider} end @@ -101,11 +99,10 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do describe "ensure_deprovisioned/1" do test "does nothing for a provider" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() {provider, _bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) assert ensure_deprovisioned(provider) == {:ok, provider} end @@ -113,11 +110,10 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do describe "authorization_uri/1" do test "returns authorization uri for a provider" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() {provider, bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) assert {:ok, authorization_uri, {state, verifier}} = authorization_uri(provider, "https://example.com/") @@ -162,13 +158,12 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do describe "verify_and_update_identity/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() {provider, bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) %{account: account, provider: provider, identity: identity, bypass: bypass} end @@ -178,10 +173,10 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do identity: identity, bypass: bypass } do - {token, claims} = AuthFixtures.generate_openid_connect_token(provider, identity) + {token, claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -213,9 +208,9 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do identity: identity, bypass: bypass } do - {token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity) + {token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{ + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{ "token_type" => "Bearer", "id_token" => token, "access_token" => "MY_ACCESS_TOKEN", @@ -223,7 +218,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do "expires_in" => 3600 }) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -244,11 +239,11 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do forty_seconds_ago = DateTime.utc_now() |> DateTime.add(-40, :second) |> DateTime.to_unix() {token, _claims} = - AuthFixtures.generate_openid_connect_token(provider, identity, %{ + Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity, %{ "exp" => forty_seconds_ago }) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -263,7 +258,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do } do token = "foo" - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -278,9 +273,11 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do bypass: bypass } do {token, _claims} = - AuthFixtures.generate_openid_connect_token(provider, identity, %{"sub" => "foo@bar.com"}) + Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity, %{ + "sub" => "foo@bar.com" + }) - AuthFixtures.expect_refresh_token(bypass, %{ + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{ "token_type" => "Bearer", "id_token" => token, "access_token" => "MY_ACCESS_TOKEN", @@ -288,7 +285,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do "expires_in" => 3600 }) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -302,10 +299,10 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do provider: provider, bypass: bypass } do - identity = AuthFixtures.create_identity(account: account) - {token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity) + identity = Fixtures.Auth.create_identity(account: account) + {token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{ + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{ "token_type" => "Bearer", "id_token" => token, "access_token" => "MY_ACCESS_TOKEN", @@ -313,7 +310,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do "expires_in" => 3600 }) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -338,13 +335,12 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do describe "refresh_token/1" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() {provider, bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) %{account: account, provider: provider, identity: identity, bypass: bypass} end @@ -354,9 +350,9 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do identity: identity, bypass: bypass } do - {token, claims} = AuthFixtures.generate_openid_connect_token(provider, identity) + {token, claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{ + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{ "token_type" => "Bearer", "id_token" => token, "access_token" => "MY_ACCESS_TOKEN", @@ -364,14 +360,14 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do "expires_in" => nil }) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_userinfo(bypass) - assert {:ok, identity, expires_at} = refresh_token(identity) + assert {:ok, provider} = refresh_access_token(provider) - assert identity.provider_state == %{ + assert %{ access_token: "MY_ACCESS_TOKEN", - claims: claims, - expires_at: expires_at, + claims: ^claims, + expires_at: _expires_at, refresh_token: "MY_REFRESH_TOKEN", userinfo: %{ "email" => "ada@example.com", @@ -384,9 +380,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg", "sub" => "353690423699814251281" } - } - - assert DateTime.diff(expires_at, DateTime.utc_now()) in 5..15 + } = provider.adapter_state end end end diff --git a/elixir/apps/domain/test/domain/auth/adapters/token_test.exs b/elixir/apps/domain/test/domain/auth/adapters/token_test.exs index 804c828cd..85afffb7e 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/token_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/token_test.exs @@ -2,12 +2,11 @@ defmodule Domain.Auth.Adapters.TokenTest do use Domain.DataCase, async: true import Domain.Auth.Adapters.Token alias Domain.Auth - alias Domain.{AccountsFixtures, AuthFixtures} describe "identity_changeset/2" do setup do - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_token_provider(account: account) + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_token_provider(account: account) %{ account: account, @@ -28,7 +27,7 @@ defmodule Domain.Auth.Adapters.TokenTest do assert %{provider_state: state, provider_virtual_state: virtual_state} = changeset.changes assert %{"secret_hash" => secret_hash} = state - assert %{secret: secret} = virtual_state + assert %{changes: %{secret: secret}} = virtual_state assert Domain.Crypto.equal?(secret, secret_hash) end @@ -76,25 +75,25 @@ defmodule Domain.Auth.Adapters.TokenTest do describe "ensure_provisioned/1" do test "does nothing for a provider" do - provider = AuthFixtures.create_token_provider() + provider = Fixtures.Auth.create_token_provider() assert ensure_provisioned(provider) == {:ok, provider} end end describe "ensure_deprovisioned/1" do test "does nothing for a provider" do - provider = AuthFixtures.create_token_provider() + provider = Fixtures.Auth.create_token_provider() assert ensure_deprovisioned(provider) == {:ok, provider} end end describe "verify_secret/2" do setup do - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_token_provider(account: account) + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_token_provider(account: account) identity = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, provider: provider, provider_virtual_state: %{ @@ -124,13 +123,13 @@ defmodule Domain.Auth.Adapters.TokenTest do ) |> Repo.update!() - assert verify_secret(identity, identity.provider_virtual_state.secret) == + assert verify_secret(identity, identity.provider_virtual_state.changes.secret) == {:error, :expired_secret} end test "returns :ok on valid secret", %{identity: identity} do assert {:ok, verified_identity, expires_at} = - verify_secret(identity, identity.provider_virtual_state.secret) + verify_secret(identity, identity.provider_virtual_state.changes.secret) assert verified_identity.provider_state["secret_hash"] == identity.provider_state["secret_hash"] diff --git a/elixir/apps/domain/test/domain/auth/adapters/userpass_test.exs b/elixir/apps/domain/test/domain/auth/adapters/userpass_test.exs index 0c951e320..1faeab647 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/userpass_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/userpass_test.exs @@ -2,12 +2,11 @@ defmodule Domain.Auth.Adapters.UserPassTest do use Domain.DataCase, async: true import Domain.Auth.Adapters.UserPass alias Domain.Auth - alias Domain.{AccountsFixtures, AuthFixtures} describe "identity_changeset/2" do setup do - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_userpass_provider(account: account) + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_userpass_provider(account: account) %{ account: account, @@ -26,12 +25,10 @@ defmodule Domain.Auth.Adapters.UserPassTest do ) assert %Ecto.Changeset{} = changeset = identity_changeset(provider, changeset) - assert %{provider_state: state, provider_virtual_state: virtual_state} = changeset.changes + assert %{provider_state: state, provider_virtual_state: %{}} = changeset.changes assert %{"password_hash" => password_hash} = state assert Domain.Crypto.equal?("Firezone1234", password_hash) - - assert virtual_state == %{} end test "returns error on invalid attrs", %{provider: provider} do @@ -51,7 +48,7 @@ defmodule Domain.Auth.Adapters.UserPassTest do assert errors_on(changeset) == %{ provider_virtual_state: %{ password: ["should be at least 12 byte(s)"], - password_confirmation: ["does not match confirmation"] + password_confirmation: ["does not match confirmation", "can't be blank"] } } @@ -100,25 +97,25 @@ defmodule Domain.Auth.Adapters.UserPassTest do describe "ensure_provisioned/1" do test "does nothing for a provider" do - provider = AuthFixtures.create_userpass_provider() + provider = Fixtures.Auth.create_userpass_provider() assert ensure_provisioned(provider) == {:ok, provider} end end describe "ensure_deprovisioned/1" do test "does nothing for a provider" do - provider = AuthFixtures.create_userpass_provider() + provider = Fixtures.Auth.create_userpass_provider() assert ensure_deprovisioned(provider) == {:ok, provider} end end describe "verify_secret/2" do setup do - account = AccountsFixtures.create_account() - provider = AuthFixtures.create_userpass_provider(account: account) + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_userpass_provider(account: account) identity = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, provider: provider, provider_virtual_state: %{ diff --git a/elixir/apps/domain/test/domain/auth_test.exs b/elixir/apps/domain/test/domain/auth_test.exs index c2665be34..f031b207b 100644 --- a/elixir/apps/domain/test/domain/auth_test.exs +++ b/elixir/apps/domain/test/domain/auth_test.exs @@ -1,10 +1,8 @@ defmodule Domain.AuthTest do use Domain.DataCase import Domain.Auth - alias Domain.ActorsFixtures alias Domain.Auth alias Domain.Auth.Authorizer - alias Domain.{AccountsFixtures, AuthFixtures} describe "list_provider_adapters/0" do test "returns list of enabled adapters for an account" do @@ -27,19 +25,19 @@ defmodule Domain.AuthTest do end test "returns error when provider is deleted" do - account = AccountsFixtures.create_account() - AuthFixtures.create_userpass_provider(account: account) + account = Fixtures.Accounts.create_account() + Fixtures.Auth.create_userpass_provider(account: account) Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) + provider = Fixtures.Auth.create_email_provider(account: account) identity = - AuthFixtures.create_identity( - actor_default_type: :account_admin_user, + Fixtures.Auth.create_identity( + actor: [type: :account_admin_user], account: account, provider: provider ) - subject = AuthFixtures.create_subject(identity) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _provider} = delete_provider(provider, subject) assert fetch_provider_by_id(provider.id) == {:error, :not_found} @@ -47,7 +45,7 @@ defmodule Domain.AuthTest do test "returns provider" do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider() + provider = Fixtures.Auth.create_email_provider() assert {:ok, fetched_provider} = fetch_provider_by_id(provider.id) assert fetched_provider.id == provider.id end @@ -55,10 +53,10 @@ defmodule Domain.AuthTest do describe "fetch_provider_by_id/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(account: account, type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -78,14 +76,14 @@ defmodule Domain.AuthTest do end test "returns error when provider is deleted", %{account: account, subject: subject} do - provider = AuthFixtures.create_userpass_provider(account: account) + provider = Fixtures.Auth.create_userpass_provider(account: account) {:ok, _provider} = delete_provider(provider, subject) assert fetch_provider_by_id(provider.id, subject) == {:error, :not_found} end test "returns provider", %{account: account, subject: subject} do - provider = AuthFixtures.create_userpass_provider(account: account) + provider = Fixtures.Auth.create_userpass_provider(account: account) assert {:ok, fetched_provider} = fetch_provider_by_id(provider.id, subject) assert fetched_provider.id == provider.id end @@ -93,10 +91,10 @@ defmodule Domain.AuthTest do describe "fetch_active_provider_by_id/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(account: account, type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -116,20 +114,20 @@ defmodule Domain.AuthTest do end test "returns error when provider is disabled", %{account: account, subject: subject} do - provider = AuthFixtures.create_userpass_provider(account: account) + provider = Fixtures.Auth.create_userpass_provider(account: account) {:ok, _provider} = disable_provider(provider, subject) assert fetch_active_provider_by_id(provider.id, subject) == {:error, :not_found} end test "returns error when provider is deleted", %{account: account, subject: subject} do - provider = AuthFixtures.create_userpass_provider(account: account) + provider = Fixtures.Auth.create_userpass_provider(account: account) {:ok, _provider} = delete_provider(provider, subject) assert fetch_active_provider_by_id(provider.id, subject) == {:error, :not_found} end test "returns provider", %{account: account, subject: subject} do - provider = AuthFixtures.create_userpass_provider(account: account) + provider = Fixtures.Auth.create_userpass_provider(account: account) assert {:ok, fetched_provider} = fetch_active_provider_by_id(provider.id, subject) assert fetched_provider.id == provider.id end @@ -141,38 +139,45 @@ defmodule Domain.AuthTest do end test "returns error when provider is disabled" do - account = AccountsFixtures.create_account() - AuthFixtures.create_userpass_provider(account: account) + account = Fixtures.Accounts.create_account() + Fixtures.Auth.create_userpass_provider(account: account) Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) + provider = Fixtures.Auth.create_email_provider(account: account) identity = - AuthFixtures.create_identity( - actor_default_type: :account_admin_user, + Fixtures.Auth.create_identity( + actor: [ + type: :account_admin_user + ], account: account, provider: provider ) - subject = AuthFixtures.create_subject(identity) + subject = + Fixtures.Auth.create_subject( + identity: identity, + actor: [type: :account_admin_user] + ) + {:ok, _provider} = disable_provider(provider, subject) assert fetch_active_provider_by_id(provider.id) == {:error, :not_found} end test "returns error when provider is deleted" do - account = AccountsFixtures.create_account() - AuthFixtures.create_userpass_provider(account: account) + account = Fixtures.Accounts.create_account() + Fixtures.Auth.create_userpass_provider(account: account) Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) + provider = Fixtures.Auth.create_email_provider(account: account) identity = - AuthFixtures.create_identity( - actor_default_type: :account_admin_user, + Fixtures.Auth.create_identity( + actor: [type: :account_admin_user], account: account, provider: provider ) - subject = AuthFixtures.create_subject(identity) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _provider} = delete_provider(provider, subject) assert fetch_active_provider_by_id(provider.id) == {:error, :not_found} @@ -180,7 +185,7 @@ defmodule Domain.AuthTest do test "returns provider" do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider() + provider = Fixtures.Auth.create_email_provider() assert {:ok, fetched_provider} = fetch_active_provider_by_id(provider.id) assert fetched_provider.id == provider.id end @@ -188,21 +193,21 @@ defmodule Domain.AuthTest do describe "list_providers_for_account/2" do test "returns all not soft-deleted providers for a given account" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - AuthFixtures.create_userpass_provider(account: account) - email_provider = AuthFixtures.create_email_provider(account: account) - token_provider = AuthFixtures.create_token_provider(account: account) + Fixtures.Auth.create_userpass_provider(account: account) + email_provider = Fixtures.Auth.create_email_provider(account: account) + token_provider = Fixtures.Auth.create_token_provider(account: account) identity = - AuthFixtures.create_identity( - actor_default_type: :account_admin_user, + Fixtures.Auth.create_identity( + actor: [type: :account_admin_user], account: account, provider: email_provider ) - subject = AuthFixtures.create_subject(identity) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _provider} = disable_provider(token_provider, subject) {:ok, _provider} = delete_provider(email_provider, subject) @@ -212,13 +217,13 @@ defmodule Domain.AuthTest do end test "returns error when subject can not manage providers" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() identity = - AuthFixtures.create_identity(actor_default_type: :account_admin_user, account: account) + Fixtures.Auth.create_identity(actor: [type: :account_admin_user], account: account) - subject = AuthFixtures.create_subject(identity) - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.create_subject(identity: identity) + subject = Fixtures.Auth.remove_permissions(subject) assert list_providers_for_account(account, subject) == {:error, @@ -228,21 +233,21 @@ defmodule Domain.AuthTest do describe "list_active_providers_for_account/1" do test "returns active providers for a given account" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - userpass_provider = AuthFixtures.create_userpass_provider(account: account) - email_provider = AuthFixtures.create_email_provider(account: account) - token_provider = AuthFixtures.create_token_provider(account: account) + userpass_provider = Fixtures.Auth.create_userpass_provider(account: account) + email_provider = Fixtures.Auth.create_email_provider(account: account) + token_provider = Fixtures.Auth.create_token_provider(account: account) identity = - AuthFixtures.create_identity( - actor_default_type: :account_admin_user, + Fixtures.Auth.create_identity( + actor: [type: :account_admin_user], account: account, provider: email_provider ) - subject = AuthFixtures.create_subject(identity) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _provider} = disable_provider(token_provider, subject) {:ok, _provider} = delete_provider(email_provider, subject) @@ -252,12 +257,129 @@ defmodule Domain.AuthTest do end end + describe "list_providers_pending_token_refresh_by_adapter/1" do + test "returns empty list if there are no providers for an adapter" do + assert list_providers_pending_token_refresh_by_adapter(:google_workspace) == {:ok, []} + end + + test "returns empty list if there are no providers with token that will expire soon" do + Fixtures.Auth.start_and_create_google_workspace_provider() + assert list_providers_pending_token_refresh_by_adapter(:google_workspace) == {:ok, []} + end + + test "ignores disabled providers" do + {provider, _bypass} = + Fixtures.Auth.start_and_create_google_workspace_provider() + + Domain.Fixture.update!(provider, %{ + disabled_at: DateTime.utc_now(), + adapter_state: %{ + "expires_at" => DateTime.utc_now() + } + }) + + assert list_providers_pending_token_refresh_by_adapter(:google_workspace) == {:ok, []} + end + + test "ignores non-custom provisioners" do + {provider, _bypass} = + Fixtures.Auth.start_and_create_google_workspace_provider() + + Domain.Fixture.update!(provider, %{ + provisioner: :manual, + adapter_state: %{ + "expires_at" => DateTime.utc_now() + } + }) + + assert list_providers_pending_token_refresh_by_adapter(:google_workspace) == {:ok, []} + end + + test "returns providers with tokens that will expire in ~1 hour" do + {provider, _bypass} = + Fixtures.Auth.start_and_create_google_workspace_provider() + + Domain.Fixture.update!(provider, %{ + adapter_state: %{ + "access_token" => "OIDC_ACCESS_TOKEN", + "refresh_token" => "OIDC_REFRESH_TOKEN", + "expires_at" => DateTime.utc_now() |> DateTime.add(58, :minute), + "claims" => "openid email profile offline_access" + } + }) + + assert {:ok, [fetched_provider]} = + list_providers_pending_token_refresh_by_adapter(:google_workspace) + + assert fetched_provider.id == provider.id + end + end + + describe "list_providers_pending_sync_by_adapter/1" do + test "returns empty list if there are no providers for an adapter" do + assert list_providers_pending_sync_by_adapter(:google_workspace) == {:ok, []} + end + + test "returns empty list if there are no providers that synced more than 10m ago" do + {provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider() + Domain.Fixture.update!(provider, %{last_synced_at: DateTime.utc_now()}) + assert list_providers_pending_sync_by_adapter(:google_workspace) == {:ok, []} + end + + test "ignores disabled providers" do + {provider, _bypass} = + Fixtures.Auth.start_and_create_google_workspace_provider() + + Domain.Fixture.update!(provider, %{ + disabled_at: DateTime.utc_now(), + adapter_state: %{ + "expires_at" => DateTime.utc_now() + } + }) + + assert list_providers_pending_sync_by_adapter(:google_workspace) == {:ok, []} + end + + test "ignores non-custom provisioners" do + {provider, _bypass} = + Fixtures.Auth.start_and_create_google_workspace_provider() + + Domain.Fixture.update!(provider, %{ + provisioner: :manual, + adapter_state: %{ + "expires_at" => DateTime.utc_now() + } + }) + + assert list_providers_pending_sync_by_adapter(:google_workspace) == {:ok, []} + end + + test "returns providers with tokens that synced more than 10m ago" do + {provider1, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider() + {provider2, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider() + + eleven_minutes_ago = DateTime.utc_now() |> DateTime.add(-11, :minute) + Domain.Fixture.update!(provider2, %{last_synced_at: eleven_minutes_ago}) + + assert {:ok, providers} = + list_providers_pending_sync_by_adapter(:google_workspace) + + assert Enum.map(providers, & &1.id) |> Enum.sort() == + Enum.sort([provider1.id, provider2.id]) + end + end + describe "new_provider/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() - {bypass, [provider_adapter_config]} = - AuthFixtures.start_openid_providers(["google"]) + bypass = Domain.Mocks.OpenIDConnect.discovery_document_server() + + provider_adapter_config = + Fixtures.Auth.openid_connect_adapter_config( + discovery_document_uri: + "http://localhost:#{bypass.port}/.well-known/openid-configuration" + ) %{ account: account, @@ -275,7 +397,7 @@ defmodule Domain.AuthTest do assert changeset.changes == %{account_id: account.id, created_by: :system} provider_attrs = - AuthFixtures.provider_attrs( + Fixtures.Auth.provider_attrs( adapter: :openid_connect, adapter_config: provider_adapter_config ) @@ -293,7 +415,7 @@ defmodule Domain.AuthTest do describe "create_provider/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() %{ account: account @@ -318,7 +440,7 @@ defmodule Domain.AuthTest do account: account } do attrs = - AuthFixtures.provider_attrs( + Fixtures.Auth.provider_attrs( name: String.duplicate("A", 256), adapter: :foo, adapter_config: :bar @@ -338,8 +460,8 @@ defmodule Domain.AuthTest do account: account } do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - AuthFixtures.create_email_provider(account: account) - attrs = AuthFixtures.provider_attrs(adapter: :email) + Fixtures.Auth.create_email_provider(account: account) + attrs = Fixtures.Auth.provider_attrs(adapter: :email) assert {:error, changeset} = create_provider(account, attrs) refute changeset.valid? assert errors_on(changeset) == %{base: ["this provider is already enabled"]} @@ -348,8 +470,8 @@ defmodule Domain.AuthTest do test "returns error if userpass provider is already enabled", %{ account: account } do - AuthFixtures.create_userpass_provider(account: account) - attrs = AuthFixtures.provider_attrs(adapter: :userpass) + Fixtures.Auth.create_userpass_provider(account: account) + attrs = Fixtures.Auth.provider_attrs(adapter: :userpass) assert {:error, changeset} = create_provider(account, attrs) refute changeset.valid? assert errors_on(changeset) == %{base: ["this provider is already enabled"]} @@ -358,8 +480,8 @@ defmodule Domain.AuthTest do test "returns error if token provider is already enabled", %{ account: account } do - AuthFixtures.create_token_provider(account: account) - attrs = AuthFixtures.provider_attrs(adapter: :token) + Fixtures.Auth.create_token_provider(account: account) + attrs = Fixtures.Auth.provider_attrs(adapter: :token) assert {:error, changeset} = create_provider(account, attrs) refute changeset.valid? assert errors_on(changeset) == %{base: ["this provider is already enabled"]} @@ -369,11 +491,10 @@ defmodule Domain.AuthTest do account: account } do {provider, _bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) attrs = - AuthFixtures.provider_attrs( + Fixtures.Auth.provider_attrs( adapter: :openid_connect, adapter_config: provider.adapter_config, provisioner: :just_in_time @@ -387,7 +508,7 @@ defmodule Domain.AuthTest do test "creates a provider", %{ account: account } do - attrs = AuthFixtures.provider_attrs() + attrs = Fixtures.Auth.provider_attrs() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) @@ -409,7 +530,7 @@ defmodule Domain.AuthTest do account: account } do Domain.Config.put_system_env_override(:outbound_email_adapter, nil) - attrs = AuthFixtures.provider_attrs() + attrs = Fixtures.Auth.provider_attrs() assert {:error, changeset} = create_provider(account, attrs) assert errors_on(changeset) == %{adapter: ["email adapter is not configured"]} @@ -418,7 +539,7 @@ defmodule Domain.AuthTest do describe "create_provider/3" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() %{ account: account @@ -429,8 +550,8 @@ defmodule Domain.AuthTest do account: account } do subject = - AuthFixtures.create_subject() - |> AuthFixtures.remove_permissions() + Fixtures.Auth.create_subject() + |> Fixtures.Auth.remove_permissions() assert create_provider(account, %{}, subject) == {:error, @@ -440,21 +561,21 @@ defmodule Domain.AuthTest do test "returns error when subject tries to create a provider in another account", %{ account: other_account } do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(account: account, type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert create_provider(other_account, %{}, subject) == {:error, :unauthorized} end test "persists identity that created the provider", %{account: account} do - attrs = AuthFixtures.provider_attrs() + attrs = Fixtures.Auth.provider_attrs() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - actor = ActorsFixtures.create_actor(account: account, type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert {:ok, provider} = create_provider(account, attrs, subject) @@ -465,11 +586,10 @@ defmodule Domain.AuthTest do describe "change_provider/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() {provider, bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_google_workspace_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) %{ account: account, @@ -479,7 +599,7 @@ defmodule Domain.AuthTest do end test "returns changeset with given changes", %{provider: provider} do - provider_attrs = AuthFixtures.provider_attrs() + provider_attrs = Fixtures.Auth.provider_attrs() assert changeset = change_provider(provider, provider_attrs) assert %Ecto.Changeset{data: %Domain.Auth.Provider{}} = changeset @@ -490,14 +610,13 @@ defmodule Domain.AuthTest do describe "update_provider/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(account: account, type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {provider, bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_google_workspace_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) %{ account: account, @@ -528,7 +647,7 @@ defmodule Domain.AuthTest do subject: subject } do attrs = - AuthFixtures.provider_attrs( + Fixtures.Auth.provider_attrs( name: String.duplicate("A", 256), adapter: :foo, adapter_config: :bar, @@ -550,8 +669,9 @@ defmodule Domain.AuthTest do subject: subject } do attrs = - AuthFixtures.provider_attrs( - provisioner: :custom, + Fixtures.Auth.provider_attrs( + provisioner: :just_in_time, + adapter: :foobar, adapter_config: %{ client_id: "foo" } @@ -572,7 +692,7 @@ defmodule Domain.AuthTest do provider: provider, subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert update_provider(provider, %{}, subject) == {:error, @@ -582,10 +702,10 @@ defmodule Domain.AuthTest do test "returns error when subject tries to update an account in another account", %{ provider: provider } do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(account: account, type: :account_admin_user) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) assert update_provider(provider, %{}, subject) == {:error, :not_found} end @@ -593,19 +713,19 @@ defmodule Domain.AuthTest do describe "disable_provider/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) + provider = Fixtures.Auth.create_email_provider(account: account) actor = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider ) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) - subject = AuthFixtures.create_subject(identity) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -620,7 +740,7 @@ defmodule Domain.AuthTest do subject: subject, provider: provider } do - other_provider = AuthFixtures.create_userpass_provider(account: account) + other_provider = Fixtures.Auth.create_userpass_provider(account: account) assert {:ok, provider} = disable_provider(provider, subject) assert provider.disabled_at @@ -643,7 +763,7 @@ defmodule Domain.AuthTest do subject: subject, provider: provider } do - AuthFixtures.create_email_provider() + Fixtures.Auth.create_email_provider() assert disable_provider(provider, subject) == {:error, :cant_disable_the_last_provider} end @@ -653,7 +773,7 @@ defmodule Domain.AuthTest do subject: subject, provider: provider } do - other_provider = AuthFixtures.create_userpass_provider(account: account) + other_provider = Fixtures.Auth.create_userpass_provider(account: account) {:ok, _other_provider} = disable_provider(other_provider, subject) assert disable_provider(provider, subject) == {:error, :cant_disable_the_last_provider} @@ -666,26 +786,26 @@ defmodule Domain.AuthTest do Task.async(fn -> allow_child_sandbox_access(test_pid) - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() - provider_one = AuthFixtures.create_email_provider(account: account) - provider_two = AuthFixtures.create_userpass_provider(account: account) + provider_one = Fixtures.Auth.create_email_provider(account: account) + provider_two = Fixtures.Auth.create_userpass_provider(account: account) actor = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider_one ) identity = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, actor: actor, provider: provider_one ) - subject = AuthFixtures.create_subject(identity) + subject = Fixtures.Auth.create_subject(identity: identity) for provider <- [provider_two, provider_one] do Task.async(fn -> @@ -709,7 +829,7 @@ defmodule Domain.AuthTest do subject: subject, account: account } do - provider = AuthFixtures.create_userpass_provider(account: account) + provider = Fixtures.Auth.create_userpass_provider(account: account) assert {:ok, _provider} = disable_provider(provider, subject) assert {:ok, provider} = disable_provider(provider, subject) assert {:ok, _provider} = disable_provider(provider, subject) @@ -718,7 +838,7 @@ defmodule Domain.AuthTest do test "does not allow to disable providers in other accounts", %{ subject: subject } do - provider = AuthFixtures.create_userpass_provider() + provider = Fixtures.Auth.create_userpass_provider() assert disable_provider(provider, subject) == {:error, :not_found} end @@ -726,7 +846,7 @@ defmodule Domain.AuthTest do subject: subject, provider: provider } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert disable_provider(provider, subject) == {:error, @@ -736,13 +856,13 @@ defmodule Domain.AuthTest do describe "enable_provider/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) + provider = Fixtures.Auth.create_email_provider(account: account) {:ok, provider} = disable_provider(provider, subject) %{ @@ -777,7 +897,7 @@ defmodule Domain.AuthTest do subject: subject } do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider() + provider = Fixtures.Auth.create_email_provider() assert enable_provider(provider, subject) == {:error, :not_found} end @@ -785,7 +905,7 @@ defmodule Domain.AuthTest do subject: subject, provider: provider } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert enable_provider(provider, subject) == {:error, @@ -795,19 +915,19 @@ defmodule Domain.AuthTest do describe "delete_provider/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) + provider = Fixtures.Auth.create_email_provider(account: account) actor = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider ) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) - subject = AuthFixtures.create_subject(identity) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -822,7 +942,7 @@ defmodule Domain.AuthTest do subject: subject, provider: provider } do - other_provider = AuthFixtures.create_userpass_provider(account: account) + other_provider = Fixtures.Auth.create_userpass_provider(account: account) assert {:ok, provider} = delete_provider(provider, subject) assert provider.deleted_at @@ -835,9 +955,11 @@ defmodule Domain.AuthTest do end test "returns error when trying to delete the last provider", %{ + account: account, subject: subject, provider: provider } do + Fixtures.Auth.create_token_provider(account: account) assert delete_provider(provider, subject) == {:error, :cant_delete_the_last_provider} end @@ -845,7 +967,7 @@ defmodule Domain.AuthTest do subject: subject, provider: provider } do - AuthFixtures.create_email_provider() + Fixtures.Auth.create_email_provider() assert delete_provider(provider, subject) == {:error, :cant_delete_the_last_provider} end @@ -855,7 +977,7 @@ defmodule Domain.AuthTest do subject: subject, provider: provider } do - other_provider = AuthFixtures.create_userpass_provider(account: account) + other_provider = Fixtures.Auth.create_userpass_provider(account: account) {:ok, _other_provider} = delete_provider(other_provider, subject) assert delete_provider(provider, subject) == {:error, :cant_delete_the_last_provider} @@ -868,26 +990,26 @@ defmodule Domain.AuthTest do Task.async(fn -> allow_child_sandbox_access(test_pid) - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() - provider_one = AuthFixtures.create_email_provider(account: account) - provider_two = AuthFixtures.create_userpass_provider(account: account) + provider_one = Fixtures.Auth.create_email_provider(account: account) + provider_two = Fixtures.Auth.create_userpass_provider(account: account) actor = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider_one ) identity = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, actor: actor, provider: provider_one ) - subject = AuthFixtures.create_subject(identity) + subject = Fixtures.Auth.create_subject(identity: identity) for provider <- [provider_two, provider_one] do Task.async(fn -> @@ -907,7 +1029,7 @@ defmodule Domain.AuthTest do subject: subject, account: account } do - provider = AuthFixtures.create_userpass_provider(account: account) + provider = Fixtures.Auth.create_userpass_provider(account: account) assert {:ok, deleted_provider} = delete_provider(provider, subject) assert delete_provider(provider, subject) == {:error, :not_found} assert delete_provider(deleted_provider, subject) == {:error, :not_found} @@ -916,7 +1038,7 @@ defmodule Domain.AuthTest do test "does not allow to delete providers in other accounts", %{ subject: subject } do - provider = AuthFixtures.create_userpass_provider() + provider = Fixtures.Auth.create_userpass_provider() assert delete_provider(provider, subject) == {:error, :not_found} end @@ -924,7 +1046,7 @@ defmodule Domain.AuthTest do subject: subject, provider: provider } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert delete_provider(provider, subject) == {:error, @@ -934,7 +1056,7 @@ defmodule Domain.AuthTest do describe "fetch_provider_capabilities!/1" do test "returns provider capabilities" do - provider = AuthFixtures.create_userpass_provider() + provider = Fixtures.Auth.create_userpass_provider() assert fetch_provider_capabilities!(provider) == [ provisioners: [:manual], @@ -950,7 +1072,7 @@ defmodule Domain.AuthTest do end test "returns identity" do - identity = AuthFixtures.create_identity() + identity = Fixtures.Auth.create_identity() assert {:ok, fetched_identity} = fetch_identity_by_id(identity.id) assert fetched_identity.id == identity.id end @@ -958,22 +1080,20 @@ defmodule Domain.AuthTest do describe "fetch_identities_count_grouped_by_provider_id/1" do test "returns count of actor identities by provider id" do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {google_provider, _bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account, name: "google") {vault_provider, _bypass} = - AuthFixtures.start_openid_providers(["vault"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account, name: "vault") - AuthFixtures.create_identity(account: account, provider: google_provider) - AuthFixtures.create_identity(account: account, provider: vault_provider) - AuthFixtures.create_identity(account: account, provider: vault_provider) + Fixtures.Auth.create_identity(account: account, provider: google_provider) + Fixtures.Auth.create_identity(account: account, provider: vault_provider) + Fixtures.Auth.create_identity(account: account, provider: vault_provider) assert fetch_identities_count_grouped_by_provider_id(subject) == {:ok, @@ -985,21 +1105,206 @@ defmodule Domain.AuthTest do end end + describe "sync_provider_identities_multi/2" do + setup do + account = Fixtures.Accounts.create_account() + + {provider, bypass} = + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) + + %{account: account, provider: provider, bypass: bypass} + end + + test "upserts new identities and actors", %{provider: provider} do + attrs_list = [ + %{ + "actor" => %{ + "name" => "Brian Manifold", + "type" => "account_user" + }, + "provider_identifier" => "USER_ID1" + }, + %{ + "actor" => %{ + "name" => "Jennie Smith", + "type" => "account_user" + }, + "provider_identifier" => "USER_ID2" + } + ] + + provider_identifiers = Enum.map(attrs_list, & &1["provider_identifier"]) + actor_names = Enum.map(attrs_list, & &1["actor"]["name"]) + + multi = sync_provider_identities_multi(provider, attrs_list) + + assert {:ok, + %{ + plan_identities: {insert, []}, + insert_identities: [_actor1, _actor2], + delete_identities: {0, nil} + }} = Repo.transaction(multi) + + assert Enum.all?(provider_identifiers, &(&1 in insert)) + + identities = Auth.Identity |> Repo.all() |> Repo.preload(:actor) + assert length(identities) == 2 + + for identity <- identities do + assert identity.inserted_at + assert identity.created_by == :provider + assert identity.provider_id == provider.id + assert identity.provider_identifier in provider_identifiers + assert identity.actor.name in actor_names + assert identity.actor.last_synced_at + end + end + + test "ignores updates to existing identities", %{account: account, provider: provider} do + Fixtures.Auth.create_identity( + account: account, + provider: provider, + provider_identifier: "USER_ID1" + ) + + Fixtures.Auth.create_identity( + account: account, + provider: provider, + provider_identifier: "USER_ID2" + ) + + attrs_list = [ + %{ + "actor" => %{ + "name" => "Brian Manifold", + "type" => "account_user" + }, + "provider_identifier" => "USER_ID1" + }, + %{ + "actor" => %{ + "name" => "Jennie Smith", + "type" => "account_user" + }, + "provider_identifier" => "USER_ID2" + } + ] + + multi = sync_provider_identities_multi(provider, attrs_list) + + assert {:ok, + %{ + plan_identities: {[], []}, + delete_identities: {0, nil}, + insert_identities: [] + }} = Repo.transaction(multi) + end + + test "deletes removed identities", %{account: account, provider: provider} do + provider_identifiers = ["USER_ID1", "USER_ID2"] + + Fixtures.Auth.create_identity( + account: account, + provider: provider, + provider_identifier: Enum.at(provider_identifiers, 0) + ) + + Fixtures.Auth.create_identity( + account: account, + provider: provider, + provider_identifier: Enum.at(provider_identifiers, 1) + ) + + attrs_list = [] + + multi = sync_provider_identities_multi(provider, attrs_list) + + assert {:ok, + %{ + plan_identities: {[], delete}, + delete_identities: {2, nil}, + insert_identities: [] + }} = Repo.transaction(multi) + + assert Enum.all?(provider_identifiers, &(&1 in delete)) + assert Repo.aggregate(Auth.Identity, :count) == 2 + assert Repo.aggregate(Auth.Identity.Query.all(), :count) == 0 + end + + test "ignores identities that are not synced from the provider", %{ + account: account, + provider: provider + } do + {other_provider, _bypass} = + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) + + Fixtures.Auth.create_identity( + account: account, + provider: other_provider, + provider_identifier: "USER_ID1" + ) + + Fixtures.Auth.create_identity( + account: account, + provider_identifier: "USER_ID2" + ) + + attrs_list = [] + + multi = sync_provider_identities_multi(provider, attrs_list) + + assert Repo.transaction(multi) == + {:ok, + %{ + identities: [], + plan_identities: {[], []}, + delete_identities: {0, nil}, + insert_identities: [] + }} + end + + test "returns error on invalid attrs", %{ + provider: provider + } do + attrs_list = [ + %{ + "actor" => %{}, + "provider_identifier" => "USER_ID2" + } + ] + + multi = sync_provider_identities_multi(provider, attrs_list) + + assert {:error, :insert_identities, changeset, _effects_so_far} = Repo.transaction(multi) + + assert errors_on(changeset) == %{ + actor: %{ + name: ["can't be blank"], + type: ["can't be blank"] + } + } + + assert Repo.aggregate(Auth.Identity, :count) == 0 + assert Repo.aggregate(Domain.Actors.Actor, :count) == 0 + end + end + describe "upsert_identity/3" do test "creates an identity" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) - provider_identifier = AuthFixtures.random_provider_identifier(provider) + provider = Fixtures.Auth.create_email_provider(account: account) + provider_identifier = Fixtures.Auth.random_provider_identifier(provider) actor = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider ) - assert {:ok, identity} = upsert_identity(actor, provider, provider_identifier) + attrs = %{provider_identifier: provider_identifier} + assert {:ok, identity} = upsert_identity(actor, provider, attrs) assert identity.provider_id == provider.id assert identity.provider_identifier == provider_identifier @@ -1013,43 +1318,236 @@ defmodule Domain.AuthTest do assert is_nil(identity.deleted_at) end - test "returns error when identifier is invalid" do - account = AccountsFixtures.create_account() + test "updates existing identity" do + account = Fixtures.Accounts.create_account() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) + provider = Fixtures.Auth.create_email_provider(account: account) + provider_identifier = Fixtures.Auth.random_provider_identifier(provider) + actor = Fixtures.Actors.create_actor(account: account, provider: provider) + + identity = + Fixtures.Auth.create_identity( + account: account, + provider: provider, + provider_identifier: provider_identifier, + actor: actor, + provider_state: %{"foo" => "bar"} + ) + + attrs = %{provider_identifier: provider_identifier} + assert {:ok, updated_identity} = upsert_identity(actor, provider, attrs) + + assert Repo.aggregate(Auth.Identity, :count) == 1 + + assert updated_identity.provider_state != identity.provider_state + end + + test "returns error when identifier is invalid" do + account = Fixtures.Accounts.create_account() + Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) + provider = Fixtures.Auth.create_email_provider(account: account) actor = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider ) - provider_identifier = Ecto.UUID.generate() - assert {:error, changeset} = upsert_identity(actor, provider, provider_identifier) + attrs = %{provider_identifier: Ecto.UUID.generate()} + assert {:error, changeset} = upsert_identity(actor, provider, attrs) assert errors_on(changeset) == %{provider_identifier: ["is an invalid email address"]} - provider_identifier = nil - assert {:error, changeset} = upsert_identity(actor, provider, provider_identifier) + attrs = %{provider_identifier: nil} + assert {:error, changeset} = upsert_identity(actor, provider, attrs) assert errors_on(changeset) == %{provider_identifier: ["can't be blank"]} end end + describe "new_identity/3" do + setup do + account = Fixtures.Accounts.create_account() + Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) + provider = Fixtures.Auth.create_email_provider(account: account) + actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user) + + %{ + account: account, + provider: provider, + actor: actor + } + end + + test "returns changeset with given changes", %{ + account: account, + provider: provider, + actor: actor + } do + account_id = account.id + actor_id = actor.id + provider_id = provider.id + + assert changeset = new_identity(actor, provider, %{}) + assert %Ecto.Changeset{data: %Domain.Auth.Identity{}} = changeset + + assert %{ + account_id: ^account_id, + actor_id: ^actor_id, + provider_id: ^provider_id, + provider_state: %{}, + provider_virtual_state: %{} + } = changeset.changes + + identity_attrs = Fixtures.Auth.identity_attrs() + + assert changeset = new_identity(actor, provider, identity_attrs) + assert %Ecto.Changeset{data: %Domain.Auth.Identity{}} = changeset + + assert %{ + account_id: ^account_id, + actor_id: ^actor_id, + provider_id: ^provider_id, + provider_state: %{}, + provider_virtual_state: %{}, + created_by: :system + } = changeset.changes + end + end + + describe "create_identity/4" do + test "creates an identity" do + account = Fixtures.Accounts.create_account() + Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) + provider = Fixtures.Auth.create_email_provider(account: account) + provider_identifier = Fixtures.Auth.random_provider_identifier(provider) + + actor = + Fixtures.Actors.create_actor( + type: :account_admin_user, + account: account, + provider: provider + ) + + subject = Fixtures.Auth.create_subject(actor: actor) + + attrs = %{provider_identifier: provider_identifier} + assert {:ok, _identity} = create_identity(actor, provider, attrs, subject) + end + + test "returns error on missing permissions" do + account = Fixtures.Accounts.create_account() + Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) + provider = Fixtures.Auth.create_email_provider(account: account) + + actor = + Fixtures.Actors.create_actor( + type: :account_admin_user, + account: account, + provider: provider + ) + + subject = + Fixtures.Auth.create_subject(actor: actor) + |> Fixtures.Auth.remove_permissions() + + assert create_identity(actor, provider, %{}, subject) == + {:error, + {:unauthorized, + [missing_permissions: [Authorizer.manage_identities_permission()]]}} + end + end + + describe "create_identity/3" do + test "creates an identity" do + account = Fixtures.Accounts.create_account() + provider = Fixtures.Auth.create_userpass_provider(account: account) + provider_identifier = Fixtures.Auth.random_provider_identifier(provider) + + actor = + Fixtures.Actors.create_actor( + type: :account_admin_user, + account: account, + provider: provider + ) + + password = "Firezone1234" + + attrs = %{ + provider_identifier: provider_identifier, + provider_virtual_state: %{"password" => password, "password_confirmation" => password} + } + + assert {:ok, identity} = create_identity(actor, provider, attrs) + + assert identity.provider_id == provider.id + assert identity.provider_identifier == provider_identifier + assert identity.actor_id == actor.id + + assert %Ecto.Changeset{} = identity.provider_virtual_state + + assert %{"password_hash" => _} = identity.provider_state + assert %{password_hash: _} = identity.provider_virtual_state.changes + assert identity.account_id == provider.account_id + assert is_nil(identity.deleted_at) + end + + test "returns error when identifier is invalid" do + account = Fixtures.Accounts.create_account() + Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) + provider = Fixtures.Auth.create_email_provider(account: account) + + actor = + Fixtures.Actors.create_actor( + type: :account_admin_user, + account: account, + provider: provider + ) + + attrs = %{provider_identifier: Ecto.UUID.generate()} + assert {:error, changeset} = create_identity(actor, provider, attrs) + assert errors_on(changeset) == %{provider_identifier: ["is an invalid email address"]} + + attrs = %{provider_identifier: nil} + assert {:error, changeset} = create_identity(actor, provider, attrs) + assert errors_on(changeset) == %{provider_identifier: ["can't be blank"]} + end + + test "updates existing identity" do + account = Fixtures.Accounts.create_account() + Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) + provider = Fixtures.Auth.create_email_provider(account: account) + provider_identifier = Fixtures.Auth.random_provider_identifier(provider) + actor = Fixtures.Actors.create_actor(account: account, provider: provider) + + Fixtures.Auth.create_identity( + account: account, + provider: provider, + provider_identifier: provider_identifier, + actor: actor, + provider_state: %{"foo" => "bar"} + ) + + attrs = %{provider_identifier: provider_identifier} + assert {:error, changeset} = create_identity(actor, provider, attrs) + assert errors_on(changeset) == %{provider_identifier: ["has already been taken"]} + end + end + describe "replace_identity/3" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) + provider = Fixtures.Auth.create_email_provider(account: account) actor = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider ) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) - subject = AuthFixtures.create_subject(identity) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -1062,20 +1560,21 @@ defmodule Domain.AuthTest do test "returns error when identity is deleted", %{identity: identity, subject: subject} do {:ok, _identity} = delete_identity(identity, subject) + attrs = %{provider_identifier: Ecto.UUID.generate()} - assert replace_identity(identity, Ecto.UUID.generate(), subject) == {:error, :not_found} + assert replace_identity(identity, attrs, subject) == {:error, :not_found} end test "replaces existing identity with a new one", %{ identity: identity, subject: subject } do - provider_identifier = Ecto.UUID.generate() - assert {:error, changeset} = replace_identity(identity, provider_identifier, subject) + attrs = %{provider_identifier: Ecto.UUID.generate()} + assert {:error, changeset} = replace_identity(identity, attrs, subject) assert errors_on(changeset) == %{provider_identifier: ["is an invalid email address"]} - provider_identifier = nil - assert {:error, changeset} = replace_identity(identity, provider_identifier, subject) + attrs = %{provider_identifier: nil} + assert {:error, changeset} = replace_identity(identity, attrs, subject) assert errors_on(changeset) == %{provider_identifier: ["can't be blank"]} refute Repo.get(Auth.Identity, identity.id).deleted_at @@ -1086,11 +1585,11 @@ defmodule Domain.AuthTest do provider: provider, subject: subject } do - provider_identifier = AuthFixtures.random_provider_identifier(provider) + attrs = %{provider_identifier: Fixtures.Auth.random_provider_identifier(provider)} - assert {:ok, new_identity} = replace_identity(identity, provider_identifier, subject) + assert {:ok, new_identity} = replace_identity(identity, attrs, subject) - assert new_identity.provider_identifier == provider_identifier + assert new_identity.provider_identifier == attrs.provider_identifier assert new_identity.provider_id == identity.provider_id assert new_identity.actor_id == identity.actor_id @@ -1108,9 +1607,10 @@ defmodule Domain.AuthTest do identity: identity, subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) + attrs = %{provider_identifier: Ecto.UUID.generate()} - assert replace_identity(identity, Ecto.UUID.generate(), subject) == + assert replace_identity(identity, attrs, subject) == {:error, {:unauthorized, [ @@ -1127,19 +1627,19 @@ defmodule Domain.AuthTest do describe "delete_identity/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) + provider = Fixtures.Auth.create_email_provider(account: account) actor = - ActorsFixtures.create_actor( + Fixtures.Actors.create_actor( type: :account_admin_user, account: account, provider: provider ) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) - subject = AuthFixtures.create_subject(identity) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -1155,7 +1655,7 @@ defmodule Domain.AuthTest do actor: actor, subject: subject } do - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) assert {:ok, deleted_identity} = delete_identity(identity, subject) @@ -1170,13 +1670,13 @@ defmodule Domain.AuthTest do provider: provider, subject: subject } do - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) subject = subject - |> AuthFixtures.remove_permissions() - |> AuthFixtures.add_permission(Authorizer.manage_own_identities_permission()) - |> AuthFixtures.add_permission(Authorizer.manage_identities_permission()) + |> Fixtures.Auth.remove_permissions() + |> Fixtures.Auth.add_permission(Authorizer.manage_own_identities_permission()) + |> Fixtures.Auth.add_permission(Authorizer.manage_identities_permission()) assert {:ok, deleted_identity} = delete_identity(identity, subject) @@ -1189,12 +1689,12 @@ defmodule Domain.AuthTest do test "does not delete identity that belongs to another actor with manage_own permission", %{ subject: subject } do - identity = AuthFixtures.create_identity() + identity = Fixtures.Auth.create_identity() subject = subject - |> AuthFixtures.remove_permissions() - |> AuthFixtures.add_permission(Authorizer.manage_own_identities_permission()) + |> Fixtures.Auth.remove_permissions() + |> Fixtures.Auth.add_permission(Authorizer.manage_own_identities_permission()) assert delete_identity(identity, subject) == {:error, :not_found} end @@ -1202,12 +1702,12 @@ defmodule Domain.AuthTest do test "does not delete identity that belongs to another actor with just view permission", %{ subject: subject } do - identity = AuthFixtures.create_identity() + identity = Fixtures.Auth.create_identity() subject = subject - |> AuthFixtures.remove_permissions() - |> AuthFixtures.add_permission(Authorizer.manage_own_identities_permission()) + |> Fixtures.Auth.remove_permissions() + |> Fixtures.Auth.add_permission(Authorizer.manage_own_identities_permission()) assert delete_identity(identity, subject) == {:error, :not_found} end @@ -1218,16 +1718,16 @@ defmodule Domain.AuthTest do actor: actor, subject: subject } do - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) assert {:ok, _identity} = delete_identity(identity, subject) assert delete_identity(identity, subject) == {:error, :not_found} end test "returns error when subject can not delete identities", %{subject: subject} do - identity = AuthFixtures.create_identity() + identity = Fixtures.Auth.create_identity() - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert delete_identity(identity, subject) == {:error, @@ -1244,13 +1744,47 @@ defmodule Domain.AuthTest do end end + describe "delete_actor_identities/1" do + setup do + account = Fixtures.Accounts.create_account() + Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) + provider = Fixtures.Auth.create_email_provider(account: account) + + %{ + account: account, + provider: provider + } + end + + test "removes all identities that belong to an actor", %{account: account, provider: provider} do + actor = Fixtures.Actors.create_actor(account: account, provider: provider) + Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + + assert Repo.aggregate(Auth.Identity.Query.all(), :count) == 3 + assert delete_actor_identities(actor) == :ok + assert Repo.aggregate(Auth.Identity.Query.all(), :count) == 0 + end + + test "does not remove identities that belong to another actor", %{ + account: account, + provider: provider + } do + actor = Fixtures.Actors.create_actor(account: account, provider: provider) + Fixtures.Auth.create_identity(account: account, provider: provider) + assert delete_actor_identities(actor) == :ok + assert Repo.aggregate(Auth.Identity.Query.all(), :count) == 1 + end + end + describe "sign_in/5" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) - user_agent = AuthFixtures.user_agent() - remote_ip = AuthFixtures.remote_ip() + provider = Fixtures.Auth.create_email_provider(account: account) + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() %{ account: account, @@ -1277,7 +1811,7 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) secret = "foo" assert sign_in(provider, identity.provider_identifier, secret, user_agent, remote_ip) == @@ -1290,7 +1824,7 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) secret = identity.provider_virtual_state.sign_in_token assert {:ok, %Auth.Subject{} = subject} = @@ -1309,7 +1843,7 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) secret = identity.provider_virtual_state.sign_in_token assert {:ok, %Auth.Subject{} = subject} = @@ -1328,8 +1862,8 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) secret = identity.provider_virtual_state.sign_in_token assert {:ok, %Auth.Subject{} = subject} = @@ -1338,8 +1872,8 @@ defmodule Domain.AuthTest do three_hours = 3 * 60 * 60 assert_datetime_diff(subject.expires_at, DateTime.utc_now(), three_hours) - actor = ActorsFixtures.create_actor(type: :account_user, account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + actor = Fixtures.Actors.create_actor(type: :account_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) secret = identity.provider_virtual_state.sign_in_token assert {:ok, %Auth.Subject{} = subject} = @@ -1355,12 +1889,12 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) - subject = AuthFixtures.create_subject(identity) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _provider} = disable_provider(provider, subject) - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) secret = identity.provider_virtual_state.sign_in_token assert sign_in(provider, identity.provider_identifier, secret, user_agent, remote_ip) == @@ -1373,10 +1907,10 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) secret = identity.provider_virtual_state.sign_in_token - subject = AuthFixtures.create_subject(identity) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, identity} = delete_identity(identity, subject) assert sign_in(provider, identity.provider_identifier, secret, user_agent, remote_ip) == @@ -1390,10 +1924,10 @@ defmodule Domain.AuthTest do remote_ip: remote_ip } do actor = - ActorsFixtures.create_actor(type: :account_admin_user, account: account) - |> ActorsFixtures.disable() + Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + |> Fixtures.Actors.disable() - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) secret = identity.provider_virtual_state.sign_in_token assert sign_in(provider, identity.provider_identifier, secret, user_agent, remote_ip) == @@ -1407,10 +1941,10 @@ defmodule Domain.AuthTest do remote_ip: remote_ip } do actor = - ActorsFixtures.create_actor(type: :account_admin_user, account: account) - |> ActorsFixtures.delete() + Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + |> Fixtures.Actors.delete() - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) secret = identity.provider_virtual_state.sign_in_token assert sign_in(provider, identity.provider_identifier, secret, user_agent, remote_ip) == @@ -1423,12 +1957,12 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) - subject = AuthFixtures.create_subject(identity) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _provider} = delete_provider(provider, subject) - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) secret = identity.provider_virtual_state.sign_in_token assert sign_in(provider, identity.provider_identifier, secret, user_agent, remote_ip) == @@ -1441,7 +1975,7 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) secret = identity.provider_virtual_state.sign_in_token assert {:ok, _subject} = @@ -1456,14 +1990,13 @@ defmodule Domain.AuthTest do describe "sign_in/4" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() {provider, bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) + Fixtures.Auth.start_and_create_openid_connect_provider(account: account) - user_agent = AuthFixtures.user_agent() - remote_ip = AuthFixtures.remote_ip() + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() %{ bypass: bypass, @@ -1481,15 +2014,15 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) {token, _claims} = - AuthFixtures.generate_openid_connect_token(provider, identity, %{ + Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity, %{ "sub" => "foo@bar.com" }) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -1505,7 +2038,7 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => "foo"}) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => "foo"}) code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -1522,17 +2055,17 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) token = - AuthFixtures.sign_openid_connect_token(%{ + Mocks.OpenIDConnect.sign_openid_connect_token(%{ "sub" => identity.provider_identifier, "aud" => provider.adapter_config["client_id"], "exp" => DateTime.utc_now() |> DateTime.add(10, :second) |> DateTime.to_unix() }) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) - AuthFixtures.expect_userinfo(bypass) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -1548,34 +2081,67 @@ defmodule Domain.AuthTest do assert subject.context.user_agent == user_agent end - # test "returned subject expiration depends on user type", %{ - # account: account, - # provider: provider, - # user_agent: user_agent, - # remote_ip: remote_ip - # } do - # actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - # identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + test "returned expiration duration is capped at one week for account users", %{ + bypass: bypass, + account: account, + provider: provider, + user_agent: user_agent, + remote_ip: remote_ip + } do + actor = Fixtures.Actors.create_actor(type: :account_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) - # code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() - # redirect_uri = "https://example.com/" - # payload = {redirect_uri, code_verifier, "MyFakeCode"} + token = + Mocks.OpenIDConnect.sign_openid_connect_token(%{ + "sub" => identity.provider_identifier, + "aud" => provider.adapter_config["client_id"], + "exp" => DateTime.utc_now() |> DateTime.add(1_000_000, :second) |> DateTime.to_unix() + }) - # assert {:ok, %Auth.Subject{} = subject} = - # sign_in(provider, payload, user_agent, remote_ip) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) - # three_hours = 3 * 60 * 60 - # assert_datetime_diff(subject.expires_at, DateTime.utc_now(), three_hours) + code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() + redirect_uri = "https://example.com/" + payload = {redirect_uri, code_verifier, "MyFakeCode"} - # actor = ActorsFixtures.create_actor(type: :account_user, account: account) - # identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + assert {:ok, %Auth.Subject{} = subject} = + sign_in(provider, payload, user_agent, remote_ip) - # assert {:ok, %Auth.Subject{} = subject} = - # sign_in(provider, payload, user_agent, remote_ip) + one_week = 7 * 24 * 60 * 60 + assert_datetime_diff(subject.expires_at, DateTime.utc_now(), one_week) + end - # one_week = 7 * 24 * 60 * 60 - # assert_datetime_diff(subject.expires_at, DateTime.utc_now(), one_week) - # end + test "returned expiration duration is capped at 3 hours for account admin users", %{ + bypass: bypass, + account: account, + provider: provider, + user_agent: user_agent, + remote_ip: remote_ip + } do + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + + token = + Mocks.OpenIDConnect.sign_openid_connect_token(%{ + "sub" => identity.provider_identifier, + "aud" => provider.adapter_config["client_id"], + "exp" => DateTime.utc_now() |> DateTime.add(1_000_000, :second) |> DateTime.to_unix() + }) + + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) + + code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() + redirect_uri = "https://example.com/" + payload = {redirect_uri, code_verifier, "MyFakeCode"} + + assert {:ok, %Auth.Subject{} = subject} = + sign_in(provider, payload, user_agent, remote_ip) + + three_hours = 3 * 60 * 60 + assert_datetime_diff(subject.expires_at, DateTime.utc_now(), three_hours) + end test "returns error when provider is disabled", %{ bypass: bypass, @@ -1584,14 +2150,14 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) - subject = AuthFixtures.create_subject(identity) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _provider} = disable_provider(provider, subject) - {token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) - AuthFixtures.expect_userinfo(bypass) + {token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -1608,14 +2174,14 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) - subject = AuthFixtures.create_subject(identity) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, identity} = delete_identity(identity, subject) - {token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) - AuthFixtures.expect_userinfo(bypass) + {token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -1633,14 +2199,14 @@ defmodule Domain.AuthTest do remote_ip: remote_ip } do actor = - ActorsFixtures.create_actor(type: :account_admin_user, account: account) - |> ActorsFixtures.disable() + Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + |> Fixtures.Actors.disable() - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) - {token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) - AuthFixtures.expect_userinfo(bypass) + {token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -1658,14 +2224,14 @@ defmodule Domain.AuthTest do remote_ip: remote_ip } do actor = - ActorsFixtures.create_actor(type: :account_admin_user, account: account) - |> ActorsFixtures.delete() + Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + |> Fixtures.Actors.delete() - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) - {token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) - AuthFixtures.expect_userinfo(bypass) + {token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -1682,14 +2248,14 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor) - subject = AuthFixtures.create_subject(identity) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _provider} = delete_provider(provider, subject) - {token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) - AuthFixtures.expect_userinfo(bypass) + {token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -1706,11 +2272,11 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = AuthFixtures.create_identity(account: account, provider: provider) + identity = Fixtures.Auth.create_identity(account: account, provider: provider) - {token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity) - AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token}) - AuthFixtures.expect_userinfo(bypass) + {token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity) + Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token}) + Mocks.OpenIDConnect.expect_userinfo(bypass) code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier() redirect_uri = "https://example.com/" @@ -1728,21 +2294,21 @@ defmodule Domain.AuthTest do describe "sign_in/3" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) - user_agent = AuthFixtures.user_agent() - remote_ip = AuthFixtures.remote_ip() + provider = Fixtures.Auth.create_email_provider(account: account) + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() identity = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, provider: provider, user_agent: user_agent, remote_ip: remote_ip ) - subject = AuthFixtures.create_subject(identity) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -1807,10 +2373,10 @@ defmodule Domain.AuthTest do subject: subject } do one_day = DateTime.utc_now() |> DateTime.add(1, :day) - provider = AuthFixtures.create_token_provider(account: account) + provider = Fixtures.Auth.create_token_provider(account: account) identity = - AuthFixtures.create_identity( + Fixtures.Auth.create_identity( account: account, provider: provider, user_agent: user_agent, @@ -1879,23 +2445,21 @@ defmodule Domain.AuthTest do describe "create_session_token_from_subject/1" do test "returns valid session token for a given subject" do - identity = AuthFixtures.create_identity() - subject = AuthFixtures.create_subject(identity) + subject = Fixtures.Auth.create_subject() assert {:ok, _token} = create_session_token_from_subject(subject) end end describe "create_client_token_from_subject/1" do test "returns valid client token for a given subject" do - identity = AuthFixtures.create_identity() - subject = AuthFixtures.create_subject(identity) + subject = Fixtures.Auth.create_subject() assert {:ok, _token} = create_client_token_from_subject(subject) end end describe "fetch_session_token_expires_at/2" do test "returns datetime when the token expires" do - subject = AuthFixtures.create_subject() + subject = Fixtures.Auth.create_subject() {:ok, token} = create_session_token_from_subject(subject) assert {:ok, expires_at} = fetch_session_token_expires_at(token) @@ -1905,17 +2469,17 @@ defmodule Domain.AuthTest do describe "has_permission?/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{account: account, actor: actor, subject: subject} end test "returns true when subject has given permission", %{subject: subject} do subject = - AuthFixtures.set_permissions(subject, [ + Fixtures.Auth.set_permissions(subject, [ Authorizer.manage_providers_permission() ]) @@ -1924,7 +2488,7 @@ defmodule Domain.AuthTest do test "returns true when subject has one of given permission", %{subject: subject} do subject = - AuthFixtures.set_permissions(subject, [ + Fixtures.Auth.set_permissions(subject, [ Authorizer.manage_providers_permission() ]) @@ -1939,7 +2503,7 @@ defmodule Domain.AuthTest do end test "returns false when subject has no given permission", %{subject: subject} do - subject = AuthFixtures.set_permissions(subject, []) + subject = Fixtures.Auth.set_permissions(subject, []) refute has_permission?(subject, Authorizer.manage_providers_permission()) end end @@ -1953,22 +2517,22 @@ defmodule Domain.AuthTest do describe "ensure_type/2" do test "returns :ok if subject actor has given type" do - subject = AuthFixtures.create_subject() + subject = Fixtures.Auth.create_subject() assert ensure_type(subject, subject.actor.type) == :ok end test "returns error if subject actor has given type" do - subject = AuthFixtures.create_subject() + subject = Fixtures.Auth.create_subject() assert ensure_type(subject, :foo) == {:error, :unauthorized} end end describe "ensure_has_access_to/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -1981,7 +2545,7 @@ defmodule Domain.AuthTest do subject: subject } do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider() + provider = Fixtures.Auth.create_email_provider() assert ensure_has_access_to(subject, provider) == {:error, :unauthorized} end @@ -1990,17 +2554,17 @@ defmodule Domain.AuthTest do account: account } do Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark) - provider = AuthFixtures.create_email_provider(account: account) + provider = Fixtures.Auth.create_email_provider(account: account) assert ensure_has_access_to(subject, provider) == :ok end end describe "ensure_has_permissions/2" do setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -2012,7 +2576,7 @@ defmodule Domain.AuthTest do test "returns error when subject has no given permissions", %{ subject: subject } do - subject = AuthFixtures.set_permissions(subject, []) + subject = Fixtures.Auth.set_permissions(subject, []) required_permissions = [Authorizer.manage_providers_permission()] @@ -2029,7 +2593,7 @@ defmodule Domain.AuthTest do subject: subject } do subject = - AuthFixtures.set_permissions(subject, [ + Fixtures.Auth.set_permissions(subject, [ Authorizer.manage_providers_permission() ]) diff --git a/elixir/apps/domain/test/domain/config/validator_test.exs b/elixir/apps/domain/test/domain/config/validator_test.exs index 38f23c2da..2a3206406 100644 --- a/elixir/apps/domain/test/domain/config/validator_test.exs +++ b/elixir/apps/domain/test/domain/config/validator_test.exs @@ -55,6 +55,7 @@ defmodule Domain.Config.ValidatorTest do {:error, [{"invalid", ["must be one of: integer, boolean"]}]} end + # TODO: uncomment once we have at least one config embed # test "validates embeds" do # type = {:json_array, {:embed, Domain.Config.Configuration.SAMLIdentityProvider}} @@ -62,7 +63,7 @@ defmodule Domain.Config.ValidatorTest do # changeset: {Domain.Config.Configuration.SAMLIdentityProvider, :create_changeset, []} # ] - # attrs = Domain.ConfigFixtures.saml_identity_providers_attrs() + # attrs = Domain.Fixtures.Config.saml_identity_providers_attrs() # assert validate(:key, [attrs], type, opts) == # {:ok, diff --git a/elixir/apps/domain/test/domain/config_test.exs b/elixir/apps/domain/test/domain/config_test.exs index 99ab6346d..f2ac5bff6 100644 --- a/elixir/apps/domain/test/domain/config_test.exs +++ b/elixir/apps/domain/test/domain/config_test.exs @@ -2,8 +2,6 @@ defmodule Domain.ConfigTest do use Domain.DataCase, async: true import Domain.Config alias Domain.Config - alias Domain.{AccountsFixtures, AuthFixtures, ActorsFixtures} - alias Domain.ConfigFixtures defmodule Test do use Domain.Config.Definition @@ -85,8 +83,8 @@ defmodule Domain.ConfigTest do describe "fetch_resolved_configs!/1" do setup do - account = AccountsFixtures.create_account() - ConfigFixtures.upsert_configuration(account: account) + account = Fixtures.Accounts.create_account() + Fixtures.Config.upsert_configuration(account: account) %{account: account} end @@ -132,8 +130,8 @@ defmodule Domain.ConfigTest do describe "fetch_resolved_configs_with_sources!/1" do setup do - account = AccountsFixtures.create_account() - ConfigFixtures.upsert_configuration(account: account) + account = Fixtures.Accounts.create_account() + Fixtures.Config.upsert_configuration(account: account) %{account: account} end @@ -336,14 +334,14 @@ defmodule Domain.ConfigTest do describe "get_account_config_by_account_id/1" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() %{account: account} end test "returns configuration for an account if it exists", %{ account: account } do - configuration = ConfigFixtures.upsert_configuration(account: account) + configuration = Fixtures.Config.upsert_configuration(account: account) assert get_account_config_by_account_id(account.id) == configuration end @@ -359,11 +357,11 @@ defmodule Domain.ConfigTest do describe "fetch_account_config/1" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -377,7 +375,7 @@ defmodule Domain.ConfigTest do account: account, subject: subject } do - configuration = ConfigFixtures.upsert_configuration(account: account) + configuration = Fixtures.Config.upsert_configuration(account: account) assert fetch_account_config(subject) == {:ok, configuration} end @@ -396,7 +394,7 @@ defmodule Domain.ConfigTest do test "returns error when subject does not have permission to read configuration", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_account_config(subject) == {:error, @@ -406,8 +404,8 @@ defmodule Domain.ConfigTest do describe "change_account_config/2" do setup do - account = AccountsFixtures.create_account() - configuration = ConfigFixtures.upsert_configuration(account: account) + account = Fixtures.Accounts.create_account() + configuration = Fixtures.Config.upsert_configuration(account: account) %{account: account, configuration: configuration} end @@ -419,14 +417,14 @@ defmodule Domain.ConfigTest do describe "update_config/3" do test "returns error when subject can not manage account configuration" do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() config = get_account_config_by_account_id(account.id) - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) subject = - AuthFixtures.create_subject(identity) - |> AuthFixtures.remove_permissions() + Fixtures.Auth.create_subject(identity: identity) + |> Fixtures.Auth.remove_permissions() assert update_config(config, %{}, subject) == {:error, @@ -436,7 +434,7 @@ defmodule Domain.ConfigTest do describe "update_config/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() %{account: account} end @@ -495,7 +493,7 @@ defmodule Domain.ConfigTest do end test "changes database config value when it existed", %{account: account} do - ConfigFixtures.upsert_configuration(account: account) + Fixtures.Config.upsert_configuration(account: account) config = get_account_config_by_account_id(account.id) attrs = %{devices_upstream_dns: ["foobar.com", "google.com"]} diff --git a/elixir/apps/domain/test/domain/devices_test.exs b/elixir/apps/domain/test/domain/devices_test.exs index a10ee2592..4a873c00e 100644 --- a/elixir/apps/domain/test/domain/devices_test.exs +++ b/elixir/apps/domain/test/domain/devices_test.exs @@ -1,23 +1,21 @@ defmodule Domain.DevicesTest do use Domain.DataCase, async: true import Domain.Devices - alias Domain.AccountsFixtures - alias Domain.{NetworkFixtures, ActorsFixtures, AuthFixtures, DevicesFixtures} alias Domain.Devices setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() - unprivileged_actor = ActorsFixtures.create_actor(type: :account_user, account: account) + unprivileged_actor = Fixtures.Actors.create_actor(type: :account_user, account: account) unprivileged_identity = - AuthFixtures.create_identity(account: account, actor: unprivileged_actor) + Fixtures.Auth.create_identity(account: account, actor: unprivileged_actor) - unprivileged_subject = AuthFixtures.create_subject(unprivileged_identity) + unprivileged_subject = Fixtures.Auth.create_subject(identity: unprivileged_identity) - admin_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - admin_identity = AuthFixtures.create_identity(account: account, actor: admin_actor) - admin_subject = AuthFixtures.create_subject(admin_identity) + admin_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + admin_identity = Fixtures.Auth.create_identity(account: account, actor: admin_actor) + admin_subject = Fixtures.Auth.create_subject(identity: admin_identity) %{ account: account, @@ -32,10 +30,10 @@ defmodule Domain.DevicesTest do describe "count_by_account_id/0" do test "counts devices for an account", %{account: account} do - DevicesFixtures.create_device(account: account) - DevicesFixtures.create_device(account: account) - DevicesFixtures.create_device(account: account) - DevicesFixtures.create_device() + Fixtures.Devices.create_device(account: account) + Fixtures.Devices.create_device(account: account) + Fixtures.Devices.create_device(account: account) + Fixtures.Devices.create_device() assert count_by_account_id(account.id) == 3 end @@ -47,7 +45,7 @@ defmodule Domain.DevicesTest do end test "returns count of devices for a actor" do - device = DevicesFixtures.create_device() + device = Fixtures.Devices.create_device() assert count_by_actor_id(device.actor_id) == 1 end end @@ -62,14 +60,14 @@ defmodule Domain.DevicesTest do unprivileged_subject: subject } do device = - DevicesFixtures.create_device(actor: actor) - |> DevicesFixtures.delete_device() + Fixtures.Devices.create_device(actor: actor) + |> Fixtures.Devices.delete_device() assert fetch_device_by_id(device.id, subject) == {:error, :not_found} end test "returns device by id", %{unprivileged_actor: actor, unprivileged_subject: subject} do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) assert fetch_device_by_id(device.id, subject) == {:ok, device} end @@ -77,12 +75,12 @@ defmodule Domain.DevicesTest do account: account, unprivileged_subject: subject } do - device = DevicesFixtures.create_device(account: account) + device = Fixtures.Devices.create_device(account: account) subject = subject - |> AuthFixtures.remove_permissions() - |> AuthFixtures.add_permission(Devices.Authorizer.manage_devices_permission()) + |> Fixtures.Auth.remove_permissions() + |> Fixtures.Auth.add_permission(Devices.Authorizer.manage_devices_permission()) assert fetch_device_by_id(device.id, subject) == {:ok, device} end @@ -90,12 +88,12 @@ defmodule Domain.DevicesTest do test "does not returns device that belongs to another account with manage permission", %{ unprivileged_subject: subject } do - device = DevicesFixtures.create_device() + device = Fixtures.Devices.create_device() subject = subject - |> AuthFixtures.remove_permissions() - |> AuthFixtures.add_permission(Devices.Authorizer.manage_devices_permission()) + |> Fixtures.Auth.remove_permissions() + |> Fixtures.Auth.add_permission(Devices.Authorizer.manage_devices_permission()) assert fetch_device_by_id(device.id, subject) == {:error, :not_found} end @@ -103,12 +101,12 @@ defmodule Domain.DevicesTest do test "does not return device that belongs to another actor with manage_own permission", %{ unprivileged_subject: subject } do - device = DevicesFixtures.create_device() + device = Fixtures.Devices.create_device() subject = subject - |> AuthFixtures.remove_permissions() - |> AuthFixtures.add_permission(Devices.Authorizer.manage_own_devices_permission()) + |> Fixtures.Auth.remove_permissions() + |> Fixtures.Auth.add_permission(Devices.Authorizer.manage_own_devices_permission()) assert fetch_device_by_id(device.id, subject) == {:error, :not_found} end @@ -121,7 +119,7 @@ defmodule Domain.DevicesTest do test "returns error when subject has no permission to view devices", %{ unprivileged_subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_device_by_id(Ecto.UUID.generate(), subject) == {:error, @@ -147,8 +145,8 @@ defmodule Domain.DevicesTest do unprivileged_actor: actor, unprivileged_subject: subject } do - DevicesFixtures.create_device(actor: actor) - |> DevicesFixtures.delete_device() + Fixtures.Devices.create_device(actor: actor) + |> Fixtures.Devices.delete_device() assert list_devices(subject) == {:ok, []} end @@ -156,7 +154,7 @@ defmodule Domain.DevicesTest do test "does not list devices in other accounts", %{ unprivileged_subject: subject } do - DevicesFixtures.create_device() + Fixtures.Devices.create_device() assert list_devices(subject) == {:ok, []} end @@ -166,8 +164,8 @@ defmodule Domain.DevicesTest do admin_actor: other_actor, unprivileged_subject: subject } do - device = DevicesFixtures.create_device(actor: actor) - DevicesFixtures.create_device(actor: other_actor) + device = Fixtures.Devices.create_device(actor: actor) + Fixtures.Devices.create_device(actor: other_actor) assert list_devices(subject) == {:ok, [device]} end @@ -177,8 +175,8 @@ defmodule Domain.DevicesTest do admin_actor: admin_actor, admin_subject: subject } do - DevicesFixtures.create_device(actor: admin_actor) - DevicesFixtures.create_device(actor: other_actor) + Fixtures.Devices.create_device(actor: admin_actor) + Fixtures.Devices.create_device(actor: other_actor) assert {:ok, devices} = list_devices(subject) assert length(devices) == 2 @@ -187,7 +185,7 @@ defmodule Domain.DevicesTest do test "returns error when subject has no permission to manage devices", %{ unprivileged_subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert list_devices(subject) == {:error, @@ -211,7 +209,7 @@ defmodule Domain.DevicesTest do } do assert list_devices_by_actor_id(Ecto.UUID.generate(), subject) == {:ok, []} assert list_devices_by_actor_id(actor.id, subject) == {:ok, []} - DevicesFixtures.create_device() + Fixtures.Devices.create_device() assert list_devices_by_actor_id(actor.id, subject) == {:ok, []} end @@ -224,8 +222,8 @@ defmodule Domain.DevicesTest do unprivileged_identity: identity, unprivileged_subject: subject } do - DevicesFixtures.create_device(identity: identity) - |> DevicesFixtures.delete_device() + Fixtures.Devices.create_device(identity: identity) + |> Fixtures.Devices.delete_device() assert list_devices_by_actor_id(actor.id, subject) == {:ok, []} end @@ -234,8 +232,8 @@ defmodule Domain.DevicesTest do unprivileged_subject: unprivileged_subject, admin_subject: admin_subject } do - actor = ActorsFixtures.create_actor(type: :account_user) - DevicesFixtures.create_device(actor: actor) + actor = Fixtures.Actors.create_actor(type: :account_user) + Fixtures.Devices.create_device(actor: actor) assert list_devices_by_actor_id(actor.id, unprivileged_subject) == {:ok, []} assert list_devices_by_actor_id(actor.id, admin_subject) == {:ok, []} @@ -246,8 +244,8 @@ defmodule Domain.DevicesTest do admin_actor: other_actor, unprivileged_subject: subject } do - device = DevicesFixtures.create_device(actor: actor) - DevicesFixtures.create_device(actor: other_actor) + device = Fixtures.Devices.create_device(actor: actor) + Fixtures.Devices.create_device(actor: other_actor) assert list_devices_by_actor_id(actor.id, subject) == {:ok, [device]} assert list_devices_by_actor_id(other_actor.id, subject) == {:ok, []} @@ -258,8 +256,8 @@ defmodule Domain.DevicesTest do admin_actor: admin_actor, admin_subject: subject } do - DevicesFixtures.create_device(actor: admin_actor) - DevicesFixtures.create_device(actor: other_actor) + Fixtures.Devices.create_device(actor: admin_actor) + Fixtures.Devices.create_device(actor: other_actor) assert {:ok, [_device]} = list_devices_by_actor_id(admin_actor.id, subject) assert {:ok, [_device]} = list_devices_by_actor_id(other_actor.id, subject) @@ -268,7 +266,7 @@ defmodule Domain.DevicesTest do test "returns error when subject has no permission to manage devices", %{ unprivileged_subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert list_devices_by_actor_id(Ecto.UUID.generate(), subject) == {:error, @@ -287,8 +285,8 @@ defmodule Domain.DevicesTest do describe "change_device/1" do test "returns changeset with given changes", %{admin_actor: actor} do - device = DevicesFixtures.create_device(actor: actor) - device_attrs = DevicesFixtures.device_attrs() + device = Fixtures.Devices.create_device(actor: actor) + device_attrs = Fixtures.Devices.device_attrs() assert changeset = change_device(device, device_attrs) assert %Ecto.Changeset{data: %Domain.Devices.Device{}} = changeset @@ -322,7 +320,7 @@ defmodule Domain.DevicesTest do admin_subject: subject } do attrs = - DevicesFixtures.device_attrs() + Fixtures.Devices.device_attrs() |> Map.delete(:name) assert {:ok, device} = upsert_device(attrs, subject) @@ -347,8 +345,8 @@ defmodule Domain.DevicesTest do test "updates device when it already exists", %{ admin_subject: subject } do - device = DevicesFixtures.create_device(subject: subject) - attrs = DevicesFixtures.device_attrs(external_id: device.external_id) + device = Fixtures.Devices.create_device(subject: subject) + attrs = Fixtures.Devices.device_attrs(external_id: device.external_id) subject = %{ subject @@ -383,10 +381,10 @@ defmodule Domain.DevicesTest do test "does not reserve additional addresses on update", %{ admin_subject: subject } do - device = DevicesFixtures.create_device(subject: subject) + device = Fixtures.Devices.create_device(subject: subject) attrs = - DevicesFixtures.device_attrs( + Fixtures.Devices.device_attrs( external_id: device.external_id, last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411", last_seen_remote_ip: %Postgrex.INET{address: {100, 64, 100, 100}} @@ -410,7 +408,7 @@ defmodule Domain.DevicesTest do admin_subject: subject } do attrs = - DevicesFixtures.device_attrs() + Fixtures.Devices.device_attrs() |> Map.delete(:name) assert {:ok, _device} = upsert_device(attrs, subject) @@ -420,7 +418,7 @@ defmodule Domain.DevicesTest do account: account, admin_subject: subject } do - attrs = DevicesFixtures.device_attrs(account: account) + attrs = Fixtures.Devices.device_attrs(account: account) assert {:ok, device} = upsert_device(attrs, subject) addresses = @@ -435,11 +433,11 @@ defmodule Domain.DevicesTest do assert %{address: device.ipv6, type: :ipv6} in addresses assert_raise Ecto.ConstraintError, fn -> - NetworkFixtures.create_address(address: device.ipv4, account: account) + Fixtures.Network.create_address(address: device.ipv4, account: account) end assert_raise Ecto.ConstraintError, fn -> - NetworkFixtures.create_address(address: device.ipv6, account: account) + Fixtures.Network.create_address(address: device.ipv6, account: account) end end @@ -447,17 +445,17 @@ defmodule Domain.DevicesTest do account: account, admin_subject: subject } do - attrs = DevicesFixtures.device_attrs(account: account) + attrs = Fixtures.Devices.device_attrs(account: account) assert {:ok, device} = upsert_device(attrs, subject) - assert %Domain.Network.Address{} = NetworkFixtures.create_address(address: device.ipv4) - assert %Domain.Network.Address{} = NetworkFixtures.create_address(address: device.ipv6) + assert %Domain.Network.Address{} = Fixtures.Network.create_address(address: device.ipv4) + assert %Domain.Network.Address{} = Fixtures.Network.create_address(address: device.ipv6) end test "returns error when subject has no permission to create devices", %{ admin_subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert upsert_device(%{}, subject) == {:error, @@ -468,7 +466,7 @@ defmodule Domain.DevicesTest do describe "update_device/3" do test "allows admin actor to update own devices", %{admin_actor: actor, admin_subject: subject} do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) attrs = %{name: "new name"} assert {:ok, device} = update_device(device, attrs, subject) @@ -480,7 +478,7 @@ defmodule Domain.DevicesTest do account: account, admin_subject: subject } do - device = DevicesFixtures.create_device(account: account) + device = Fixtures.Devices.create_device(account: account) attrs = %{name: "new name"} assert {:ok, device} = update_device(device, attrs, subject) @@ -492,7 +490,7 @@ defmodule Domain.DevicesTest do unprivileged_actor: actor, unprivileged_subject: subject } do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) attrs = %{name: "new name"} assert {:ok, device} = update_device(device, attrs, subject) @@ -504,7 +502,7 @@ defmodule Domain.DevicesTest do account: account, unprivileged_subject: subject } do - device = DevicesFixtures.create_device(account: account) + device = Fixtures.Devices.create_device(account: account) attrs = %{name: "new name"} assert update_device(device, attrs, subject) == @@ -516,7 +514,7 @@ defmodule Domain.DevicesTest do test "does not allow admin actor to update devices in other accounts", %{ admin_subject: subject } do - device = DevicesFixtures.create_device() + device = Fixtures.Devices.create_device() attrs = %{name: "new name"} assert update_device(device, attrs, subject) == {:error, :not_found} @@ -526,7 +524,7 @@ defmodule Domain.DevicesTest do admin_actor: actor, admin_subject: subject } do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) attrs = %{name: nil, public_key: nil} assert {:error, changeset} = update_device(device, attrs, subject) @@ -535,7 +533,7 @@ defmodule Domain.DevicesTest do end test "returns error on invalid attrs", %{admin_actor: actor, admin_subject: subject} do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) attrs = %{ name: String.duplicate("a", 256) @@ -552,7 +550,7 @@ defmodule Domain.DevicesTest do admin_actor: actor, admin_subject: subject } do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) fields = Devices.Device.__schema__(:fields) -- [:name] value = -1 @@ -567,16 +565,16 @@ defmodule Domain.DevicesTest do admin_actor: actor, admin_subject: subject } do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert update_device(device, %{}, subject) == {:error, {:unauthorized, [missing_permissions: [Devices.Authorizer.manage_own_devices_permission()]]}} - device = DevicesFixtures.create_device() + device = Fixtures.Devices.create_device() assert update_device(device, %{}, subject) == {:error, @@ -587,7 +585,7 @@ defmodule Domain.DevicesTest do describe "delete_device/2" do test "returns error on state conflict", %{admin_actor: actor, admin_subject: subject} do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) assert {:ok, deleted} = delete_device(device, subject) assert delete_device(deleted, subject) == {:error, :not_found} @@ -595,7 +593,7 @@ defmodule Domain.DevicesTest do end test "admin can delete own devices", %{admin_actor: actor, admin_subject: subject} do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) assert {:ok, deleted} = delete_device(device, subject) assert deleted.deleted_at @@ -605,7 +603,7 @@ defmodule Domain.DevicesTest do unprivileged_actor: actor, admin_subject: subject } do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) assert {:ok, deleted} = delete_device(device, subject) assert deleted.deleted_at @@ -614,7 +612,7 @@ defmodule Domain.DevicesTest do test "admin can not delete devices in other accounts", %{ admin_subject: subject } do - device = DevicesFixtures.create_device() + device = Fixtures.Devices.create_device() assert delete_device(device, subject) == {:error, :not_found} end @@ -624,7 +622,7 @@ defmodule Domain.DevicesTest do unprivileged_actor: actor, unprivileged_subject: subject } do - device = DevicesFixtures.create_device(account: account, actor: actor) + device = Fixtures.Devices.create_device(account: account, actor: actor) assert {:ok, deleted} = delete_device(device, subject) assert deleted.deleted_at @@ -634,14 +632,14 @@ defmodule Domain.DevicesTest do account: account, unprivileged_subject: subject } do - device = DevicesFixtures.create_device() + device = Fixtures.Devices.create_device() assert delete_device(device, subject) == {:error, {:unauthorized, [missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}} - device = DevicesFixtures.create_device(account: account) + device = Fixtures.Devices.create_device(account: account) assert delete_device(device, subject) == {:error, @@ -655,16 +653,16 @@ defmodule Domain.DevicesTest do admin_actor: actor, admin_subject: subject } do - device = DevicesFixtures.create_device(actor: actor) + device = Fixtures.Devices.create_device(actor: actor) - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert delete_device(device, subject) == {:error, {:unauthorized, [missing_permissions: [Devices.Authorizer.manage_own_devices_permission()]]}} - device = DevicesFixtures.create_device() + device = Fixtures.Devices.create_device() assert delete_device(device, subject) == {:error, @@ -672,4 +670,25 @@ defmodule Domain.DevicesTest do [missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}} end end + + describe "delete_actor_devices/1" do + test "removes all devices that belong to an actor" do + actor = Fixtures.Actors.create_actor() + Fixtures.Devices.create_device(actor: actor) + Fixtures.Devices.create_device(actor: actor) + Fixtures.Devices.create_device(actor: actor) + + assert Repo.aggregate(Devices.Device.Query.all(), :count) == 3 + assert delete_actor_devices(actor) == :ok + assert Repo.aggregate(Devices.Device.Query.all(), :count) == 0 + end + + test "does not remove devices that belong to another actor" do + actor = Fixtures.Actors.create_actor() + Fixtures.Devices.create_device() + + assert delete_actor_devices(actor) == :ok + assert Repo.aggregate(Devices.Device.Query.all(), :count) == 1 + end + end end diff --git a/elixir/apps/domain/test/domain/gateways_test.exs b/elixir/apps/domain/test/domain/gateways_test.exs index 00c1335be..7fee3425c 100644 --- a/elixir/apps/domain/test/domain/gateways_test.exs +++ b/elixir/apps/domain/test/domain/gateways_test.exs @@ -1,15 +1,13 @@ defmodule Domain.GatewaysTest do use Domain.DataCase, async: true import Domain.Gateways - alias Domain.{AccountsFixtures, ResourcesFixtures} - alias Domain.{NetworkFixtures, ActorsFixtures, AuthFixtures, GatewaysFixtures} alias Domain.Gateways setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -27,7 +25,7 @@ defmodule Domain.GatewaysTest do test "does not return groups from other accounts", %{ subject: subject } do - group = GatewaysFixtures.create_group() + group = Fixtures.Gateways.create_group() assert fetch_group_by_id(group.id, subject) == {:error, :not_found} end @@ -36,14 +34,14 @@ defmodule Domain.GatewaysTest do subject: subject } do group = - GatewaysFixtures.create_group(account: account) - |> GatewaysFixtures.delete_group() + Fixtures.Gateways.create_group(account: account) + |> Fixtures.Gateways.delete_group() assert fetch_group_by_id(group.id, subject) == {:error, :not_found} end test "returns group by id", %{account: account, subject: subject} do - group = GatewaysFixtures.create_group(account: account) + group = Fixtures.Gateways.create_group(account: account) assert {:ok, fetched_group} = fetch_group_by_id(group.id, subject) assert fetched_group.id == group.id end @@ -52,7 +50,7 @@ defmodule Domain.GatewaysTest do account: account, subject: subject } do - group = GatewaysFixtures.create_group(account: account) + group = Fixtures.Gateways.create_group(account: account) assert {:ok, fetched_group} = fetch_group_by_id(group.id, subject) assert fetched_group.id == group.id end @@ -65,7 +63,7 @@ defmodule Domain.GatewaysTest do test "returns error when subject has no permission to view groups", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_group_by_id(Ecto.UUID.generate(), subject) == {:error, @@ -82,7 +80,7 @@ defmodule Domain.GatewaysTest do test "does not list groups from other accounts", %{ subject: subject } do - GatewaysFixtures.create_group() + Fixtures.Gateways.create_group() assert list_groups(subject) == {:ok, []} end @@ -90,8 +88,8 @@ defmodule Domain.GatewaysTest do account: account, subject: subject } do - GatewaysFixtures.create_group(account: account) - |> GatewaysFixtures.delete_group() + Fixtures.Gateways.create_group(account: account) + |> Fixtures.Gateways.delete_group() assert list_groups(subject) == {:ok, []} end @@ -100,9 +98,9 @@ defmodule Domain.GatewaysTest do account: account, subject: subject } do - GatewaysFixtures.create_group(account: account) - GatewaysFixtures.create_group(account: account) - GatewaysFixtures.create_group() + Fixtures.Gateways.create_group(account: account) + Fixtures.Gateways.create_group(account: account) + Fixtures.Gateways.create_group() assert {:ok, groups} = list_groups(subject) assert length(groups) == 2 @@ -111,7 +109,7 @@ defmodule Domain.GatewaysTest do test "returns error when subject has no permission to manage groups", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert list_groups(subject) == {:error, @@ -156,7 +154,7 @@ defmodule Domain.GatewaysTest do assert {:error, changeset} = create_group(attrs, subject) assert "should be at most 64 characters long" in errors_on(changeset).tags - GatewaysFixtures.create_group(account: account, name_prefix: "foo") + Fixtures.Gateways.create_group(account: account, name_prefix: "foo") attrs = %{name_prefix: "foo", tokens: [%{}]} assert {:error, changeset} = create_group(attrs, subject) assert "has already been taken" in errors_on(changeset).name_prefix @@ -185,7 +183,7 @@ defmodule Domain.GatewaysTest do test "returns error when subject has no permission to manage groups", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert create_group(%{}, subject) == {:error, @@ -196,10 +194,10 @@ defmodule Domain.GatewaysTest do describe "change_group/1" do test "returns changeset with given changes" do - group = GatewaysFixtures.create_group() + group = Fixtures.Gateways.create_group() group_attrs = - GatewaysFixtures.group_attrs() + Fixtures.Gateways.group_attrs() |> Map.delete(:tokens) assert changeset = change_group(group, group_attrs) @@ -212,7 +210,7 @@ defmodule Domain.GatewaysTest do test "does not allow to reset required fields to empty values", %{ subject: subject } do - group = GatewaysFixtures.create_group() + group = Fixtures.Gateways.create_group() attrs = %{name_prefix: nil} assert {:error, changeset} = update_group(group, attrs, subject) @@ -221,7 +219,7 @@ defmodule Domain.GatewaysTest do end test "returns error on invalid attrs", %{account: account, subject: subject} do - group = GatewaysFixtures.create_group(account: account) + group = Fixtures.Gateways.create_group(account: account) attrs = %{ name_prefix: String.duplicate("A", 65), @@ -243,14 +241,14 @@ defmodule Domain.GatewaysTest do assert {:error, changeset} = update_group(group, attrs, subject) assert "should be at most 64 characters long" in errors_on(changeset).tags - GatewaysFixtures.create_group(account: account, name_prefix: "foo") + Fixtures.Gateways.create_group(account: account, name_prefix: "foo") attrs = %{name_prefix: "foo"} assert {:error, changeset} = update_group(group, attrs, subject) assert "has already been taken" in errors_on(changeset).name_prefix end test "updates a group", %{account: account, subject: subject} do - group = GatewaysFixtures.create_group(account: account) + group = Fixtures.Gateways.create_group(account: account) attrs = %{ name_prefix: "foo", @@ -266,9 +264,9 @@ defmodule Domain.GatewaysTest do account: account, subject: subject } do - group = GatewaysFixtures.create_group(account: account) + group = Fixtures.Gateways.create_group(account: account) - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert update_group(group, %{}, subject) == {:error, @@ -279,7 +277,7 @@ defmodule Domain.GatewaysTest do describe "delete_group/2" do test "returns error on state conflict", %{account: account, subject: subject} do - group = GatewaysFixtures.create_group(account: account) + group = Fixtures.Gateways.create_group(account: account) assert {:ok, deleted} = delete_group(group, subject) assert delete_group(deleted, subject) == {:error, :not_found} @@ -287,16 +285,16 @@ defmodule Domain.GatewaysTest do end test "deletes groups", %{account: account, subject: subject} do - group = GatewaysFixtures.create_group(account: account) + group = Fixtures.Gateways.create_group(account: account) assert {:ok, deleted} = delete_group(group, subject) assert deleted.deleted_at end test "deletes all tokens when group is deleted", %{account: account, subject: subject} do - group = GatewaysFixtures.create_group(account: account) - GatewaysFixtures.create_token(group: group) - GatewaysFixtures.create_token(group: [account: account]) + group = Fixtures.Gateways.create_group(account: account) + Fixtures.Gateways.create_token(group: group) + Fixtures.Gateways.create_token(group: [account: account]) assert {:ok, deleted} = delete_group(group, subject) assert deleted.deleted_at @@ -312,9 +310,9 @@ defmodule Domain.GatewaysTest do test "returns error when subject has no permission to delete groups", %{ subject: subject } do - group = GatewaysFixtures.create_group() + group = Fixtures.Gateways.create_group() - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert delete_group(group, subject) == {:error, @@ -325,7 +323,7 @@ defmodule Domain.GatewaysTest do describe "use_token_by_id_and_secret/2" do test "returns token when secret is valid" do - token = GatewaysFixtures.create_token() + token = Fixtures.Gateways.create_token() assert {:ok, token} = use_token_by_id_and_secret(token.id, token.value) assert is_nil(token.value) # TODO: While we don't have token rotation implemented, the tokens are all multi-use @@ -335,7 +333,7 @@ defmodule Domain.GatewaysTest do # TODO: While we don't have token rotation implemented, the tokens are all multi-use # test "returns error when secret was already used" do - # token = GatewaysFixtures.create_token() + # token = Fixtures.Gateways.create_token() # assert {:ok, _token} = use_token_by_id_and_secret(token.id, token.value) # assert use_token_by_id_and_secret(token.id, token.value) == {:error, :not_found} @@ -350,7 +348,7 @@ defmodule Domain.GatewaysTest do end test "returns error when secret is invalid" do - token = GatewaysFixtures.create_token() + token = Fixtures.Gateways.create_token() assert use_token_by_id_and_secret(token.id, "bar") == {:error, :not_found} end end @@ -363,7 +361,7 @@ defmodule Domain.GatewaysTest do test "does not return gateways from other accounts", %{ subject: subject } do - gateway = GatewaysFixtures.create_gateway() + gateway = Fixtures.Gateways.create_gateway() assert fetch_gateway_by_id(gateway.id, subject) == {:error, :not_found} end @@ -372,14 +370,14 @@ defmodule Domain.GatewaysTest do subject: subject } do gateway = - GatewaysFixtures.create_gateway(account: account) - |> GatewaysFixtures.delete_gateway() + Fixtures.Gateways.create_gateway(account: account) + |> Fixtures.Gateways.delete_gateway() assert fetch_gateway_by_id(gateway.id, subject) == {:error, :not_found} end test "returns gateway by id", %{account: account, subject: subject} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) assert fetch_gateway_by_id(gateway.id, subject) == {:ok, gateway} end @@ -387,7 +385,7 @@ defmodule Domain.GatewaysTest do account: account, subject: subject } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) assert fetch_gateway_by_id(gateway.id, subject) == {:ok, gateway} end @@ -399,7 +397,7 @@ defmodule Domain.GatewaysTest do test "returns error when subject has no permission to view gateways", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_gateway_by_id(Ecto.UUID.generate(), subject) == {:error, @@ -417,7 +415,7 @@ defmodule Domain.GatewaysTest do # TODO: add a test that soft-deleted assocs are not preloaded test "associations are preloaded when opts given", %{account: account, subject: subject} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) {:ok, gateway} = fetch_gateway_by_id(gateway.id, subject, preload: [:group, :account]) assert Ecto.assoc_loaded?(gateway.group) == true @@ -433,8 +431,8 @@ defmodule Domain.GatewaysTest do test "does not list deleted gateways", %{ subject: subject } do - GatewaysFixtures.create_gateway() - |> GatewaysFixtures.delete_gateway() + Fixtures.Gateways.create_gateway() + |> Fixtures.Gateways.delete_gateway() assert list_gateways(subject) == {:ok, []} end @@ -443,10 +441,10 @@ defmodule Domain.GatewaysTest do account: account, subject: subject } do - offline_gateway = GatewaysFixtures.create_gateway(account: account) - online_gateway = GatewaysFixtures.create_gateway(account: account) + offline_gateway = Fixtures.Gateways.create_gateway(account: account) + online_gateway = Fixtures.Gateways.create_gateway(account: account) :ok = connect_gateway(online_gateway) - GatewaysFixtures.create_gateway() + Fixtures.Gateways.create_gateway() assert {:ok, gateways} = list_gateways(subject) assert length(gateways) == 2 @@ -463,7 +461,7 @@ defmodule Domain.GatewaysTest do test "returns error when subject has no permission to manage gateways", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert list_gateways(subject) == {:error, @@ -473,8 +471,8 @@ defmodule Domain.GatewaysTest do # TODO: add a test that soft-deleted assocs are not preloaded test "associations are preloaded when opts given", %{account: account, subject: subject} do - GatewaysFixtures.create_gateway(account: account) - GatewaysFixtures.create_gateway(account: account) + Fixtures.Gateways.create_gateway(account: account) + Fixtures.Gateways.create_gateway(account: account) {:ok, gateways} = list_gateways(subject, preload: [:group, :account]) assert length(gateways) == 2 @@ -486,23 +484,23 @@ defmodule Domain.GatewaysTest do describe "list_connected_gateways_for_resource/1" do test "returns empty list when there are no online gateways", %{account: account} do - resource = ResourcesFixtures.create_resource(account: account) + resource = Fixtures.Resources.create_resource(account: account) - GatewaysFixtures.create_gateway(account: account) + Fixtures.Gateways.create_gateway(account: account) - GatewaysFixtures.create_gateway(account: account) - |> GatewaysFixtures.delete_gateway() + Fixtures.Gateways.create_gateway(account: account) + |> Fixtures.Gateways.delete_gateway() assert list_connected_gateways_for_resource(resource) == {:ok, []} end test "returns list of connected gateways for a given resource", %{account: account} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) resource = - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: gateway.group_id}] + connections: [%{gateway_group_id: gateway.group_id}] ) assert connect_gateway(gateway) == :ok @@ -514,8 +512,8 @@ defmodule Domain.GatewaysTest do test "does not return connected gateways that are not connected to given resource", %{ account: account } do - resource = ResourcesFixtures.create_resource(account: account) - gateway = GatewaysFixtures.create_gateway(account: account) + resource = Fixtures.Resources.create_resource(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) assert connect_gateway(gateway) == :ok @@ -525,30 +523,30 @@ defmodule Domain.GatewaysTest do describe "gateway_can_connect_to_resource?/2" do test "returns true when gateway can connect to resource", %{account: account} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) :ok = connect_gateway(gateway) resource = - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: gateway.group_id}] + connections: [%{gateway_group_id: gateway.group_id}] ) assert gateway_can_connect_to_resource?(gateway, resource) end test "returns false when gateway cannot connect to resource", %{account: account} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) :ok = connect_gateway(gateway) - resource = ResourcesFixtures.create_resource(account: account) + resource = Fixtures.Resources.create_resource(account: account) refute gateway_can_connect_to_resource?(gateway, resource) end test "returns false when gateway is offline", %{account: account} do - gateway = GatewaysFixtures.create_gateway(account: account) - resource = ResourcesFixtures.create_resource(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) + resource = Fixtures.Resources.create_resource(account: account) refute gateway_can_connect_to_resource?(gateway, resource) end @@ -556,8 +554,8 @@ defmodule Domain.GatewaysTest do describe "change_gateway/1" do test "returns changeset with given changes" do - gateway = GatewaysFixtures.create_gateway() - gateway_attrs = GatewaysFixtures.gateway_attrs() + gateway = Fixtures.Gateways.create_gateway() + gateway_attrs = Fixtures.Gateways.gateway_attrs() assert changeset = change_gateway(gateway, gateway_attrs) assert %Ecto.Changeset{data: %Domain.Gateways.Gateway{}} = changeset @@ -568,7 +566,7 @@ defmodule Domain.GatewaysTest do describe "upsert_gateway/3" do setup context do - token = GatewaysFixtures.create_token(account: context.account) + token = Fixtures.Gateways.create_token(account: context.account) context |> Map.put(:token, token) @@ -598,7 +596,7 @@ defmodule Domain.GatewaysTest do token: token } do attrs = - GatewaysFixtures.gateway_attrs() + Fixtures.Gateways.gateway_attrs() |> Map.delete(:name) assert {:ok, gateway} = upsert_gateway(token, attrs) @@ -621,10 +619,10 @@ defmodule Domain.GatewaysTest do test "updates gateway when it already exists", %{ token: token } do - gateway = GatewaysFixtures.create_gateway(token: token) + gateway = Fixtures.Gateways.create_gateway(token: token) attrs = - GatewaysFixtures.gateway_attrs( + Fixtures.Gateways.gateway_attrs( external_id: gateway.external_id, last_seen_remote_ip: {100, 64, 100, 101}, last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411" @@ -655,10 +653,10 @@ defmodule Domain.GatewaysTest do test "does not reserve additional addresses on update", %{ token: token } do - gateway = GatewaysFixtures.create_gateway(token: token) + gateway = Fixtures.Gateways.create_gateway(token: token) attrs = - GatewaysFixtures.gateway_attrs( + Fixtures.Gateways.gateway_attrs( external_id: gateway.external_id, last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411", last_seen_remote_ip: %Postgrex.INET{address: {100, 64, 100, 100}} @@ -682,7 +680,7 @@ defmodule Domain.GatewaysTest do account: account, token: token } do - attrs = GatewaysFixtures.gateway_attrs() + attrs = Fixtures.Gateways.gateway_attrs() assert {:ok, gateway} = upsert_gateway(token, attrs) addresses = @@ -697,14 +695,14 @@ defmodule Domain.GatewaysTest do assert %{address: gateway.ipv6, type: :ipv6} in addresses assert_raise Ecto.ConstraintError, fn -> - NetworkFixtures.create_address(account: account, address: gateway.ipv4) + Fixtures.Network.create_address(account: account, address: gateway.ipv4) end end end describe "update_gateway/3" do test "updates gateways", %{account: account, subject: subject} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) attrs = %{name_suffix: "Foo"} assert {:ok, gateway} = update_gateway(gateway, attrs, subject) @@ -716,7 +714,7 @@ defmodule Domain.GatewaysTest do account: account, subject: subject } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) attrs = %{name_suffix: nil} assert {:error, changeset} = update_gateway(gateway, attrs, subject) @@ -725,7 +723,7 @@ defmodule Domain.GatewaysTest do end test "returns error on invalid attrs", %{account: account, subject: subject} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) attrs = %{ name_suffix: String.duplicate("a", 256) @@ -742,7 +740,7 @@ defmodule Domain.GatewaysTest do account: account, subject: subject } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) fields = Gateways.Gateway.__schema__(:fields) -- [:name_suffix] value = -1 @@ -756,9 +754,9 @@ defmodule Domain.GatewaysTest do test "returns error when subject has no permission to update gateways", %{ subject: subject } do - gateway = GatewaysFixtures.create_gateway() + gateway = Fixtures.Gateways.create_gateway() - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert update_gateway(gateway, %{}, subject) == {:error, @@ -769,7 +767,7 @@ defmodule Domain.GatewaysTest do describe "delete_gateway/2" do test "returns error on state conflict", %{account: account, subject: subject} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) assert {:ok, deleted} = delete_gateway(gateway, subject) assert delete_gateway(deleted, subject) == {:error, :not_found} @@ -777,7 +775,7 @@ defmodule Domain.GatewaysTest do end test "deletes gateways", %{account: account, subject: subject} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) assert {:ok, deleted} = delete_gateway(gateway, subject) assert deleted.deleted_at @@ -786,9 +784,9 @@ defmodule Domain.GatewaysTest do test "returns error when subject has no permission to delete gateways", %{ subject: subject } do - gateway = GatewaysFixtures.create_gateway() + gateway = Fixtures.Gateways.create_gateway() - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert delete_gateway(gateway, subject) == {:error, @@ -799,26 +797,26 @@ defmodule Domain.GatewaysTest do describe "load_balance_gateways/1" do test "returns random gateway" do - gateways = Enum.map(1..10, fn _ -> GatewaysFixtures.create_gateway() end) + gateways = Enum.map(1..10, fn _ -> Fixtures.Gateways.create_gateway() end) assert Enum.member?(gateways, load_balance_gateways(gateways)) end end describe "load_balance_gateways/2" do test "returns random gateway if no gateways are already connected" do - gateways = Enum.map(1..10, fn _ -> GatewaysFixtures.create_gateway() end) + gateways = Enum.map(1..10, fn _ -> Fixtures.Gateways.create_gateway() end) assert Enum.member?(gateways, load_balance_gateways(gateways, [])) end test "reuses gateway that is already connected to reduce the latency" do - gateways = Enum.map(1..10, fn _ -> GatewaysFixtures.create_gateway() end) + gateways = Enum.map(1..10, fn _ -> Fixtures.Gateways.create_gateway() end) [connected_gateway | _] = gateways assert load_balance_gateways(gateways, [connected_gateway.id]) == connected_gateway end test "returns random gateway from the connected ones" do - gateways = Enum.map(1..10, fn _ -> GatewaysFixtures.create_gateway() end) + gateways = Enum.map(1..10, fn _ -> Fixtures.Gateways.create_gateway() end) [connected_gateway1, connected_gateway2 | _] = gateways assert load_balance_gateways(gateways, [connected_gateway1.id, connected_gateway2.id]) in [ @@ -830,7 +828,7 @@ defmodule Domain.GatewaysTest do describe "encode_token!/1" do test "returns encoded token" do - token = GatewaysFixtures.create_token() + token = Fixtures.Gateways.create_token() assert encrypted_secret = encode_token!(token) config = Application.fetch_env!(:domain, Domain.Gateways) @@ -844,7 +842,7 @@ defmodule Domain.GatewaysTest do describe "authorize_gateway/1" do test "returns token when encoded secret is valid" do - token = GatewaysFixtures.create_token() + token = Fixtures.Gateways.create_token() encoded_token = encode_token!(token) assert {:ok, fetched_token} = authorize_gateway(encoded_token) assert fetched_token.id == token.id diff --git a/elixir/apps/domain/test/domain/network/address/query_test.exs b/elixir/apps/domain/test/domain/network/address/query_test.exs index 2ccd07e58..5333176ce 100644 --- a/elixir/apps/domain/test/domain/network/address/query_test.exs +++ b/elixir/apps/domain/test/domain/network/address/query_test.exs @@ -1,10 +1,9 @@ defmodule Domain.Network.Address.QueryTest do use Domain.DataCase, async: true import Domain.Network.Address.Query - alias Domain.{AccountsFixtures, NetworkFixtures} setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() %{account: account} end @@ -23,7 +22,7 @@ defmodule Domain.Network.Address.QueryTest do offset = 3 queryable = next_available_address(account.id, cidr, offset) - NetworkFixtures.create_address(account: account, address: "10.3.3.3") + Fixtures.Network.create_address(account: account, address: "10.3.3.3") assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 3, 4}} end @@ -33,7 +32,7 @@ defmodule Domain.Network.Address.QueryTest do offset = 3 queryable = next_available_address(account.id, cidr, offset) - NetworkFixtures.create_address(address: "10.3.3.3") + Fixtures.Network.create_address(address: "10.3.3.3") assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 3, 3}} end @@ -46,11 +45,11 @@ defmodule Domain.Network.Address.QueryTest do queryable = next_available_address(account.id, cidr, offset) - NetworkFixtures.create_address(account: account, address: "10.3.4.3") - NetworkFixtures.create_address(account: account, address: "10.3.4.4") + Fixtures.Network.create_address(account: account, address: "10.3.4.3") + Fixtures.Network.create_address(account: account, address: "10.3.4.4") assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 4, 5}} - NetworkFixtures.create_address(account: account, address: "10.3.4.5") + Fixtures.Network.create_address(account: account, address: "10.3.4.5") assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 4, 6}} end @@ -62,13 +61,13 @@ defmodule Domain.Network.Address.QueryTest do queryable = next_available_address(account.id, cidr, offset) - NetworkFixtures.create_address(account: account, address: "10.3.5.5") - NetworkFixtures.create_address(account: account, address: "10.3.5.6") + Fixtures.Network.create_address(account: account, address: "10.3.5.5") + Fixtures.Network.create_address(account: account, address: "10.3.5.6") # Notice: end of range is 10.3.5.7 # but it's a broadcast address that we don't allow to assign assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 5, 4}} - NetworkFixtures.create_address(account: account, address: "10.3.5.4") + Fixtures.Network.create_address(account: account, address: "10.3.5.4") assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 5, 3}} end @@ -76,8 +75,8 @@ defmodule Domain.Network.Address.QueryTest do cidr = string_to_cidr("10.3.6.0/30") offset = 1 - NetworkFixtures.create_address(account: account, address: "10.3.6.1") - NetworkFixtures.create_address(account: account, address: "10.3.6.2") + Fixtures.Network.create_address(account: account, address: "10.3.6.1") + Fixtures.Network.create_address(account: account, address: "10.3.6.2") queryable = next_available_address(account.id, cidr, offset) assert is_nil(Repo.one(queryable)) @@ -157,7 +156,7 @@ defmodule Domain.Network.Address.QueryTest do cidr = string_to_cidr("fd00::3:2:0/126") offset = 3 - NetworkFixtures.create_address(account: account, address: "fd00::3:2:2") + Fixtures.Network.create_address(account: account, address: "fd00::3:2:2") queryable = next_available_address(account.id, cidr, offset) assert is_nil(Repo.one(queryable)) diff --git a/elixir/apps/domain/test/domain/network_test.exs b/elixir/apps/domain/test/domain/network_test.exs index 5e9550742..8e1834af5 100644 --- a/elixir/apps/domain/test/domain/network_test.exs +++ b/elixir/apps/domain/test/domain/network_test.exs @@ -1,11 +1,10 @@ defmodule Domain.NetworkTest do use Domain.DataCase, async: true - alias Domain.AccountsFixtures import Domain.Network describe "fetch_next_available_address!/2" do setup do - account = AccountsFixtures.create_account() + account = Fixtures.Accounts.create_account() %{account: account} end diff --git a/elixir/apps/domain/test/domain/policies_test.exs b/elixir/apps/domain/test/domain/policies_test.exs index 54f842dc1..805ce5e39 100644 --- a/elixir/apps/domain/test/domain/policies_test.exs +++ b/elixir/apps/domain/test/domain/policies_test.exs @@ -2,22 +2,13 @@ defmodule Domain.PoliciesTest do alias Web.Policies use Domain.DataCase, async: true import Domain.Policies - - alias Domain.{ - AccountsFixtures, - ActorsFixtures, - AuthFixtures, - PoliciesFixtures, - ResourcesFixtures - } - alias Domain.Policies setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -37,7 +28,7 @@ defmodule Domain.PoliciesTest do end test "returns policy when policy exists", %{account: account, subject: subject} do - policy = PoliciesFixtures.create_policy(account: account) + policy = Fixtures.Policies.create_policy(account: account) assert {:ok, fetched_policy} = fetch_policy_by_id(policy.id, subject) assert fetched_policy.id == policy.id @@ -45,19 +36,19 @@ defmodule Domain.PoliciesTest do test "does not return deleted policy", %{account: account, subject: subject} do {:ok, policy} = - PoliciesFixtures.create_policy(account: account) + Fixtures.Policies.create_policy(account: account) |> delete_policy(subject) assert fetch_policy_by_id(policy.id, subject) == {:error, :not_found} end test "does not return policies in other accounts", %{subject: subject} do - policy = PoliciesFixtures.create_policy() + policy = Fixtures.Policies.create_policy() assert fetch_policy_by_id(policy.id, subject) == {:error, :not_found} end test "returns error when subject has no permission to view policies", %{subject: subject} do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_policy_by_id(Ecto.UUID.generate(), subject) == {:error, @@ -75,7 +66,7 @@ defmodule Domain.PoliciesTest do # TODO: add a test that soft-deleted assocs are not preloaded test "associations are preloaded when opts given", %{account: account, subject: subject} do - policy = PoliciesFixtures.create_policy(account: account) + policy = Fixtures.Policies.create_policy(account: account) {:ok, policy} = fetch_policy_by_id(policy.id, subject, preload: [:actor_group, :resource]) assert Ecto.assoc_loaded?(policy.actor_group) @@ -89,39 +80,37 @@ defmodule Domain.PoliciesTest do end test "does not list policies from other accounts", %{subject: subject} do - PoliciesFixtures.create_policy() + Fixtures.Policies.create_policy() assert list_policies(subject) == {:ok, []} end test "does not list deleted policies", %{account: account, subject: subject} do - PoliciesFixtures.create_policy(account: account) + Fixtures.Policies.create_policy(account: account) |> delete_policy(subject) assert list_policies(subject) == {:ok, []} end test "returns all policies for account admin subject", %{account: account} do - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) - PoliciesFixtures.create_policy(account: account) - PoliciesFixtures.create_policy(account: account) - PoliciesFixtures.create_policy() + Fixtures.Policies.create_policy(account: account) + Fixtures.Policies.create_policy(account: account) + Fixtures.Policies.create_policy() assert {:ok, policies} = list_policies(subject) assert length(policies) == 2 end test "returns select policies for non-admin subject", %{account: account, subject: subject} do - unprivileged_actor = ActorsFixtures.create_actor(type: :account_user, account: account) + unprivileged_actor = Fixtures.Actors.create_actor(type: :account_user, account: account) - unpriviledged_identity = - AuthFixtures.create_identity(account: account, actor: unprivileged_actor) + unprivileged_subject = + Fixtures.Auth.create_subject(account: account, identity: [actor: unprivileged_actor]) - unprivileged_subject = AuthFixtures.create_subject(unpriviledged_identity) - - actor_group = ActorsFixtures.create_group(account: account, subject: subject) + actor_group = Fixtures.Actors.create_group(account: account, subject: subject) Domain.Actors.update_group( actor_group, @@ -129,16 +118,16 @@ defmodule Domain.PoliciesTest do subject ) - PoliciesFixtures.create_policy(account: account, actor_group: actor_group) - PoliciesFixtures.create_policy(account: account) - PoliciesFixtures.create_policy() + Fixtures.Policies.create_policy(account: account, actor_group: actor_group) + Fixtures.Policies.create_policy(account: account) + Fixtures.Policies.create_policy() assert {:ok, policies} = list_policies(unprivileged_subject) assert length(policies) == 1 end test "returns error when subject has no permission to view policies", %{subject: subject} do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert list_policies(subject) == {:error, @@ -177,7 +166,7 @@ defmodule Domain.PoliciesTest do end test "returns error when subject has no permission to manage policies", %{subject: subject} do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert create_policy(%{}, subject) == {:error, @@ -194,10 +183,10 @@ defmodule Domain.PoliciesTest do account: account, subject: subject } do - other_account = AccountsFixtures.create_account() + other_account = Fixtures.Accounts.create_account() - resource = ResourcesFixtures.create_resource(account: account) - other_actor_group = ActorsFixtures.create_group(account: other_account) + resource = Fixtures.Resources.create_resource(account: account) + other_actor_group = Fixtures.Actors.create_group(account: other_account) attrs = %{ account_id: account.id, @@ -215,10 +204,10 @@ defmodule Domain.PoliciesTest do account: account, subject: subject } do - other_account = AccountsFixtures.create_account() + other_account = Fixtures.Accounts.create_account() - other_resource = ResourcesFixtures.create_resource(account: other_account) - actor_group = ActorsFixtures.create_group(account: account) + other_resource = Fixtures.Resources.create_resource(account: other_account) + actor_group = Fixtures.Actors.create_group(account: account) attrs = %{ account_id: account.id, @@ -237,7 +226,7 @@ defmodule Domain.PoliciesTest do describe "update_policy/3" do setup context do policy = - PoliciesFixtures.create_policy( + Fixtures.Policies.create_policy( account: context.account, subject: context.subject ) @@ -250,7 +239,7 @@ defmodule Domain.PoliciesTest do end test "returns changeset error on invalid params", %{account: account, subject: subject} do - policy = PoliciesFixtures.create_policy(account: account, subject: subject) + policy = Fixtures.Policies.create_policy(account: account, subject: subject) assert {:error, changeset} = update_policy( @@ -274,7 +263,7 @@ defmodule Domain.PoliciesTest do account: account, subject: subject } do - new_actor_group = ActorsFixtures.create_group(account: account) + new_actor_group = Fixtures.Actors.create_group(account: account) assert {:ok, updated_policy} = update_policy(policy, %{actor_group_id: new_actor_group.id}, subject) @@ -287,7 +276,7 @@ defmodule Domain.PoliciesTest do account: account, subject: subject } do - new_resource = ResourcesFixtures.create_resource(account: account) + new_resource = Fixtures.Resources.create_resource(account: account) assert {:ok, updated_policy} = update_policy(policy, %{resource_id: new_resource.id}, subject) @@ -299,7 +288,7 @@ defmodule Domain.PoliciesTest do policy: policy, subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert update_policy(policy, %{name: "Name Change Attempt"}, subject) == {:error, @@ -312,10 +301,13 @@ defmodule Domain.PoliciesTest do end test "return error when subject is outside of account", %{policy: policy} do - other_account = AccountsFixtures.create_account() - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: other_account) - other_identity = AuthFixtures.create_identity(account: other_account, actor: other_actor) - other_subject = AuthFixtures.create_subject(other_identity) + other_account = Fixtures.Accounts.create_account() + + other_actor = + Fixtures.Actors.create_actor(type: :account_admin_user, account: other_account) + + other_identity = Fixtures.Auth.create_identity(account: other_account, actor: other_actor) + other_subject = Fixtures.Auth.create_subject(identity: other_identity) assert {:error, :unauthorized} = update_policy(policy, %{name: "Should not be allowed"}, other_subject) @@ -325,7 +317,7 @@ defmodule Domain.PoliciesTest do describe "delete_policy/2" do setup context do policy = - PoliciesFixtures.create_policy( + Fixtures.Policies.create_policy( account: context.account, subject: context.subject ) @@ -342,7 +334,7 @@ defmodule Domain.PoliciesTest do policy: policy, subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert delete_policy(policy, subject) == {:error, @@ -363,11 +355,7 @@ defmodule Domain.PoliciesTest do test "returns error when subject attempts to delete policy outside of account", %{ policy: policy } do - other_account = AccountsFixtures.create_account() - other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: other_account) - other_identity = AuthFixtures.create_identity(account: other_account, actor: other_actor) - other_subject = AuthFixtures.create_subject(other_identity) - + other_subject = Fixtures.Auth.create_subject() assert delete_policy(policy, other_subject) == {:error, :not_found} end end diff --git a/elixir/apps/domain/test/domain/relays_test.exs b/elixir/apps/domain/test/domain/relays_test.exs index f6578489d..238038477 100644 --- a/elixir/apps/domain/test/domain/relays_test.exs +++ b/elixir/apps/domain/test/domain/relays_test.exs @@ -1,15 +1,13 @@ defmodule Domain.RelaysTest do use Domain.DataCase, async: true import Domain.Relays - alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures, ResourcesFixtures} - alias Domain.RelaysFixtures alias Domain.Relays setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -27,7 +25,7 @@ defmodule Domain.RelaysTest do test "does not return groups from other accounts", %{ subject: subject } do - group = RelaysFixtures.create_group() + group = Fixtures.Relays.create_group() assert fetch_group_by_id(group.id, subject) == {:error, :not_found} end @@ -36,14 +34,14 @@ defmodule Domain.RelaysTest do subject: subject } do group = - RelaysFixtures.create_group(account: account) - |> RelaysFixtures.delete_group() + Fixtures.Relays.create_group(account: account) + |> Fixtures.Relays.delete_group() assert fetch_group_by_id(group.id, subject) == {:error, :not_found} end test "returns group by id", %{account: account, subject: subject} do - group = RelaysFixtures.create_group(account: account) + group = Fixtures.Relays.create_group(account: account) assert {:ok, fetched_group} = fetch_group_by_id(group.id, subject) assert fetched_group.id == group.id end @@ -51,7 +49,7 @@ defmodule Domain.RelaysTest do test "returns global group by id", %{ subject: subject } do - group = RelaysFixtures.create_global_group() + group = Fixtures.Relays.create_global_group() assert {:ok, fetched_group} = fetch_group_by_id(group.id, subject) assert fetched_group.id == group.id end @@ -60,7 +58,7 @@ defmodule Domain.RelaysTest do account: account, subject: subject } do - group = RelaysFixtures.create_group(account: account) + group = Fixtures.Relays.create_group(account: account) assert {:ok, fetched_group} = fetch_group_by_id(group.id, subject) assert fetched_group.id == group.id end @@ -73,7 +71,7 @@ defmodule Domain.RelaysTest do test "returns error when subject has no permission to view groups", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_group_by_id(Ecto.UUID.generate(), subject) == {:error, @@ -90,7 +88,7 @@ defmodule Domain.RelaysTest do test "does not list groups from other accounts", %{ subject: subject } do - RelaysFixtures.create_group() + Fixtures.Relays.create_group() assert list_groups(subject) == {:ok, []} end @@ -98,8 +96,8 @@ defmodule Domain.RelaysTest do account: account, subject: subject } do - RelaysFixtures.create_group(account: account) - |> RelaysFixtures.delete_group() + Fixtures.Relays.create_group(account: account) + |> Fixtures.Relays.delete_group() assert list_groups(subject) == {:ok, []} end @@ -108,16 +106,16 @@ defmodule Domain.RelaysTest do account: account, subject: subject } do - RelaysFixtures.create_group(account: account) - RelaysFixtures.create_group(account: account) - RelaysFixtures.create_group() + Fixtures.Relays.create_group(account: account) + Fixtures.Relays.create_group(account: account) + Fixtures.Relays.create_group() assert {:ok, groups} = list_groups(subject) assert length(groups) == 2 end test "returns global groups", %{subject: subject} do - RelaysFixtures.create_global_group() + Fixtures.Relays.create_global_group() assert {:ok, [_group]} = list_groups(subject) end @@ -125,7 +123,7 @@ defmodule Domain.RelaysTest do test "returns error when subject has no permission to manage groups", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert list_groups(subject) == {:error, @@ -160,7 +158,7 @@ defmodule Domain.RelaysTest do name: ["should be at most 64 character(s)"] } - RelaysFixtures.create_group(account: account, name: "foo") + Fixtures.Relays.create_group(account: account, name: "foo") attrs = %{name: "foo", tokens: [%{}]} assert {:error, changeset} = create_group(attrs, subject) assert "has already been taken" in errors_on(changeset).name @@ -187,7 +185,7 @@ defmodule Domain.RelaysTest do test "returns error when subject has no permission to manage groups", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert create_group(%{}, subject) == {:error, @@ -214,7 +212,7 @@ defmodule Domain.RelaysTest do name: ["should be at most 64 character(s)"] } - RelaysFixtures.create_global_group(name: "foo") + Fixtures.Relays.create_global_group(name: "foo") attrs = %{name: "foo", tokens: [%{}]} assert {:error, changeset} = create_global_group(attrs) assert "has already been taken" in errors_on(changeset).name @@ -241,10 +239,10 @@ defmodule Domain.RelaysTest do describe "change_group/1" do test "returns changeset with given changes" do - group = RelaysFixtures.create_group() + group = Fixtures.Relays.create_group() group_attrs = - RelaysFixtures.group_attrs() + Fixtures.Relays.group_attrs() |> Map.delete(:tokens) assert changeset = change_group(group, group_attrs) @@ -257,7 +255,7 @@ defmodule Domain.RelaysTest do test "does not allow to reset required fields to empty values", %{ subject: subject } do - group = RelaysFixtures.create_group() + group = Fixtures.Relays.create_group() attrs = %{name: nil} assert {:error, changeset} = update_group(group, attrs, subject) @@ -266,7 +264,7 @@ defmodule Domain.RelaysTest do end test "returns error on invalid attrs", %{account: account, subject: subject} do - group = RelaysFixtures.create_group(account: account) + group = Fixtures.Relays.create_group(account: account) attrs = %{ name: String.duplicate("A", 65) @@ -278,14 +276,14 @@ defmodule Domain.RelaysTest do name: ["should be at most 64 character(s)"] } - RelaysFixtures.create_group(account: account, name: "foo") + Fixtures.Relays.create_group(account: account, name: "foo") attrs = %{name: "foo"} assert {:error, changeset} = update_group(group, attrs, subject) assert "has already been taken" in errors_on(changeset).name end test "updates a group", %{account: account, subject: subject} do - group = RelaysFixtures.create_group(account: account) + group = Fixtures.Relays.create_group(account: account) attrs = %{ name: "foo" @@ -296,7 +294,7 @@ defmodule Domain.RelaysTest do end test "does not allow updating global group", %{subject: subject} do - group = RelaysFixtures.create_global_group() + group = Fixtures.Relays.create_global_group() attrs = %{name: "foo"} assert update_group(group, attrs, subject) == {:error, :unauthorized} end @@ -305,9 +303,9 @@ defmodule Domain.RelaysTest do account: account, subject: subject } do - group = RelaysFixtures.create_group(account: account) + group = Fixtures.Relays.create_group(account: account) - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert update_group(group, %{}, subject) == {:error, @@ -318,7 +316,7 @@ defmodule Domain.RelaysTest do describe "delete_group/2" do test "returns error on state conflict", %{account: account, subject: subject} do - group = RelaysFixtures.create_group(account: account) + group = Fixtures.Relays.create_group(account: account) assert {:ok, deleted} = delete_group(group, subject) assert delete_group(deleted, subject) == {:error, :not_found} @@ -326,21 +324,21 @@ defmodule Domain.RelaysTest do end test "deletes groups", %{account: account, subject: subject} do - group = RelaysFixtures.create_group(account: account) + group = Fixtures.Relays.create_group(account: account) assert {:ok, deleted} = delete_group(group, subject) assert deleted.deleted_at end test "does not allow deleting global group", %{subject: subject} do - group = RelaysFixtures.create_global_group() + group = Fixtures.Relays.create_global_group() assert delete_group(group, subject) == {:error, :unauthorized} end test "deletes all tokens when group is deleted", %{account: account, subject: subject} do - group = RelaysFixtures.create_group(account: account) - RelaysFixtures.create_token(group: group) - RelaysFixtures.create_token(group: [account: account]) + group = Fixtures.Relays.create_group(account: account) + Fixtures.Relays.create_token(group: group) + Fixtures.Relays.create_token(group: [account: account]) assert {:ok, deleted} = delete_group(group, subject) assert deleted.deleted_at @@ -356,9 +354,9 @@ defmodule Domain.RelaysTest do test "returns error when subject has no permission to delete groups", %{ subject: subject } do - group = RelaysFixtures.create_group() + group = Fixtures.Relays.create_group() - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert delete_group(group, subject) == {:error, @@ -369,7 +367,7 @@ defmodule Domain.RelaysTest do describe "use_token_by_id_and_secret/2" do test "returns token when secret is valid" do - token = RelaysFixtures.create_token() + token = Fixtures.Relays.create_token() assert {:ok, token} = use_token_by_id_and_secret(token.id, token.value) assert is_nil(token.value) # TODO: While we don't have token rotation implemented, the tokens are all multi-use @@ -379,7 +377,7 @@ defmodule Domain.RelaysTest do # TODO: While we don't have token rotation implemented, the tokens are all multi-use # test "returns error when secret was already used" do - # token = RelaysFixtures.create_token() + # token = Fixtures.Relays.create_token() # assert {:ok, _token} = use_token_by_id_and_secret(token.id, token.value) # assert use_token_by_id_and_secret(token.id, token.value) == {:error, :not_found} @@ -394,7 +392,7 @@ defmodule Domain.RelaysTest do end test "returns error when secret is invalid" do - token = RelaysFixtures.create_token() + token = Fixtures.Relays.create_token() assert use_token_by_id_and_secret(token.id, "bar") == {:error, :not_found} end end @@ -407,7 +405,7 @@ defmodule Domain.RelaysTest do test "does not return relays from other accounts", %{ subject: subject } do - relay = RelaysFixtures.create_relay() + relay = Fixtures.Relays.create_relay() assert fetch_relay_by_id(relay.id, subject) == {:error, :not_found} end @@ -416,14 +414,14 @@ defmodule Domain.RelaysTest do subject: subject } do relay = - RelaysFixtures.create_relay(account: account) - |> RelaysFixtures.delete_relay() + Fixtures.Relays.create_relay(account: account) + |> Fixtures.Relays.delete_relay() assert fetch_relay_by_id(relay.id, subject) == {:error, :not_found} end test "returns relay by id", %{account: account, subject: subject} do - relay = RelaysFixtures.create_relay(account: account) + relay = Fixtures.Relays.create_relay(account: account) assert fetch_relay_by_id(relay.id, subject) == {:ok, relay} end @@ -431,7 +429,7 @@ defmodule Domain.RelaysTest do account: account, subject: subject } do - relay = RelaysFixtures.create_relay(account: account) + relay = Fixtures.Relays.create_relay(account: account) assert fetch_relay_by_id(relay.id, subject) == {:ok, relay} end @@ -443,7 +441,7 @@ defmodule Domain.RelaysTest do test "returns error when subject has no permission to view relays", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_relay_by_id(Ecto.UUID.generate(), subject) == {:error, @@ -460,8 +458,8 @@ defmodule Domain.RelaysTest do test "does not list deleted relays", %{ subject: subject } do - RelaysFixtures.create_relay() - |> RelaysFixtures.delete_relay() + Fixtures.Relays.create_relay() + |> Fixtures.Relays.delete_relay() assert list_relays(subject) == {:ok, []} end @@ -470,12 +468,12 @@ defmodule Domain.RelaysTest do account: account, subject: subject } do - RelaysFixtures.create_relay(account: account) - RelaysFixtures.create_relay(account: account) - RelaysFixtures.create_relay() + Fixtures.Relays.create_relay(account: account) + Fixtures.Relays.create_relay(account: account) + Fixtures.Relays.create_relay() - group = RelaysFixtures.create_global_group() - relay = RelaysFixtures.create_relay(group: group) + group = Fixtures.Relays.create_global_group() + relay = Fixtures.Relays.create_relay(group: group) assert {:ok, relays} = list_relays(subject) assert length(relays) == 3 @@ -490,7 +488,7 @@ defmodule Domain.RelaysTest do test "returns error when subject has no permission to manage relays", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert list_relays(subject) == {:error, @@ -501,19 +499,19 @@ defmodule Domain.RelaysTest do describe "list_connected_relays_for_resource/1" do test "returns empty list when there are no online relays", %{account: account} do - resource = ResourcesFixtures.create_resource(account: account) + resource = Fixtures.Resources.create_resource(account: account) - RelaysFixtures.create_relay(account: account) + Fixtures.Relays.create_relay(account: account) - RelaysFixtures.create_relay(account: account) - |> RelaysFixtures.delete_relay() + Fixtures.Relays.create_relay(account: account) + |> Fixtures.Relays.delete_relay() assert list_connected_relays_for_resource(resource) == {:ok, []} end test "returns list of connected account relays", %{account: account} do - resource = ResourcesFixtures.create_resource(account: account) - relay = RelaysFixtures.create_relay(account: account) + resource = Fixtures.Resources.create_resource(account: account) + relay = Fixtures.Relays.create_relay(account: account) stamp_secret = Ecto.UUID.generate() assert connect_relay(relay, stamp_secret) == :ok @@ -525,9 +523,9 @@ defmodule Domain.RelaysTest do end test "returns list of connected global relays", %{account: account} do - resource = ResourcesFixtures.create_resource(account: account) - group = RelaysFixtures.create_global_group() - relay = RelaysFixtures.create_relay(group: group) + resource = Fixtures.Resources.create_resource(account: account) + group = Fixtures.Relays.create_global_group() + relay = Fixtures.Relays.create_relay(group: group) stamp_secret = Ecto.UUID.generate() assert connect_relay(relay, stamp_secret) == :ok @@ -541,7 +539,7 @@ defmodule Domain.RelaysTest do describe "generate_username_and_password/1" do test "returns username and password", %{account: account} do - relay = RelaysFixtures.create_relay(account: account) + relay = Fixtures.Relays.create_relay(account: account) stamp_secret = Ecto.UUID.generate() relay = %{relay | stamp_secret: stamp_secret} expires_at = DateTime.utc_now() |> DateTime.add(3, :second) @@ -563,7 +561,7 @@ defmodule Domain.RelaysTest do describe "upsert_relay/3" do setup context do - token = RelaysFixtures.create_token(account: context.account) + token = Fixtures.Relays.create_token(account: context.account) context |> Map.put(:token, token) @@ -599,7 +597,7 @@ defmodule Domain.RelaysTest do token: token } do attrs = - RelaysFixtures.relay_attrs() + Fixtures.Relays.relay_attrs() |> Map.delete(:name) assert {:ok, relay} = upsert_relay(token, attrs) @@ -623,7 +621,7 @@ defmodule Domain.RelaysTest do token: token } do attrs = - RelaysFixtures.relay_attrs() + Fixtures.Relays.relay_attrs() |> Map.drop([:name, :ipv4]) assert {:ok, _relay} = upsert_relay(token, attrs) @@ -635,10 +633,10 @@ defmodule Domain.RelaysTest do test "updates relay when it already exists", %{ token: token } do - relay = RelaysFixtures.create_relay(token: token) + relay = Fixtures.Relays.create_relay(token: token) attrs = - RelaysFixtures.relay_attrs( + Fixtures.Relays.relay_attrs( ipv4: relay.ipv4, last_seen_remote_ip: relay.ipv4, last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411" @@ -667,12 +665,12 @@ defmodule Domain.RelaysTest do end test "updates global relay when it already exists" do - group = RelaysFixtures.create_global_group() + group = Fixtures.Relays.create_global_group() token = hd(group.tokens) - relay = RelaysFixtures.create_relay(group: group, token: token) + relay = Fixtures.Relays.create_relay(group: group, token: token) attrs = - RelaysFixtures.relay_attrs( + Fixtures.Relays.relay_attrs( ipv4: relay.ipv4, last_seen_remote_ip: relay.ipv4, last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411" @@ -703,7 +701,7 @@ defmodule Domain.RelaysTest do describe "delete_relay/2" do test "returns error on state conflict", %{account: account, subject: subject} do - relay = RelaysFixtures.create_relay(account: account) + relay = Fixtures.Relays.create_relay(account: account) assert {:ok, deleted} = delete_relay(relay, subject) assert delete_relay(deleted, subject) == {:error, :not_found} @@ -711,7 +709,7 @@ defmodule Domain.RelaysTest do end test "deletes relays", %{account: account, subject: subject} do - relay = RelaysFixtures.create_relay(account: account) + relay = Fixtures.Relays.create_relay(account: account) assert {:ok, deleted} = delete_relay(relay, subject) assert deleted.deleted_at @@ -720,9 +718,9 @@ defmodule Domain.RelaysTest do test "returns error when subject has no permission to delete relays", %{ subject: subject } do - relay = RelaysFixtures.create_relay() + relay = Fixtures.Relays.create_relay() - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert delete_relay(relay, subject) == {:error, @@ -733,7 +731,7 @@ defmodule Domain.RelaysTest do describe "encode_token!/1" do test "returns encoded token" do - token = RelaysFixtures.create_token() + token = Fixtures.Relays.create_token() assert encrypted_secret = encode_token!(token) config = Application.fetch_env!(:domain, Domain.Relays) @@ -747,7 +745,7 @@ defmodule Domain.RelaysTest do describe "authorize_relay/1" do test "returns token when encoded secret is valid" do - token = RelaysFixtures.create_token() + token = Fixtures.Relays.create_token() encoded_token = encode_token!(token) assert {:ok, fetched_token} = authorize_relay(encoded_token) assert fetched_token.id == token.id diff --git a/elixir/apps/domain/test/domain/resources_test.exs b/elixir/apps/domain/test/domain/resources_test.exs index 02668d8d7..9cf20d40e 100644 --- a/elixir/apps/domain/test/domain/resources_test.exs +++ b/elixir/apps/domain/test/domain/resources_test.exs @@ -1,15 +1,13 @@ defmodule Domain.ResourcesTest do use Domain.DataCase, async: true import Domain.Resources - alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures, GatewaysFixtures, NetworkFixtures} - alias Domain.ResourcesFixtures alias Domain.Resources setup do - account = AccountsFixtures.create_account() - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) %{ account: account, @@ -29,7 +27,7 @@ defmodule Domain.ResourcesTest do end test "returns resource when resource exists", %{account: account, subject: subject} do - resource = ResourcesFixtures.create_resource(account: account) + resource = Fixtures.Resources.create_resource(account: account) assert {:ok, fetched_resource} = fetch_resource_by_id(resource.id, subject) assert fetched_resource.id == resource.id @@ -37,19 +35,19 @@ defmodule Domain.ResourcesTest do test "does not return deleted resources", %{account: account, subject: subject} do {:ok, resource} = - ResourcesFixtures.create_resource(account: account) + Fixtures.Resources.create_resource(account: account) |> delete_resource(subject) assert fetch_resource_by_id(resource.id, subject) == {:error, :not_found} end test "does not return resources in other accounts", %{subject: subject} do - resource = ResourcesFixtures.create_resource() + resource = Fixtures.Resources.create_resource() assert fetch_resource_by_id(resource.id, subject) == {:error, :not_found} end test "returns error when subject has no permission to view resources", %{subject: subject} do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert fetch_resource_by_id(Ecto.UUID.generate(), subject) == {:error, @@ -67,7 +65,7 @@ defmodule Domain.ResourcesTest do # TODO: add a test that soft-deleted assocs are not preloaded test "associations are preloaded when opts given", %{account: account, subject: subject} do - resource = ResourcesFixtures.create_resource(account: account) + resource = Fixtures.Resources.create_resource(account: account) {:ok, resource} = fetch_resource_by_id(resource.id, subject, preload: :connections) assert Ecto.assoc_loaded?(resource.connections) == true @@ -82,7 +80,7 @@ defmodule Domain.ResourcesTest do test "does not list resources from other accounts", %{ subject: subject } do - ResourcesFixtures.create_resource() + Fixtures.Resources.create_resource() assert list_resources(subject) == {:ok, []} end @@ -90,7 +88,7 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - ResourcesFixtures.create_resource(account: account) + Fixtures.Resources.create_resource(account: account) |> delete_resource(subject) assert list_resources(subject) == {:ok, []} @@ -99,13 +97,13 @@ defmodule Domain.ResourcesTest do test "returns all resources for account admin subject", %{ account: account } do - actor = ActorsFixtures.create_actor(type: :account_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - subject = AuthFixtures.create_subject(identity) + actor = Fixtures.Actors.create_actor(type: :account_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) - ResourcesFixtures.create_resource(account: account) - ResourcesFixtures.create_resource(account: account) - ResourcesFixtures.create_resource() + Fixtures.Resources.create_resource(account: account) + Fixtures.Resources.create_resource(account: account) + Fixtures.Resources.create_resource() assert {:ok, resources} = list_resources(subject) assert length(resources) == 2 @@ -115,9 +113,9 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - ResourcesFixtures.create_resource(account: account) - ResourcesFixtures.create_resource(account: account) - ResourcesFixtures.create_resource() + Fixtures.Resources.create_resource(account: account) + Fixtures.Resources.create_resource(account: account) + Fixtures.Resources.create_resource() assert {:ok, resources} = list_resources(subject) assert length(resources) == 2 @@ -126,7 +124,7 @@ defmodule Domain.ResourcesTest do test "returns error when subject has no permission to manage resources", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert list_resources(subject) == {:error, @@ -148,7 +146,7 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) assert list_resources_for_gateway(gateway, subject) == {:ok, []} end @@ -157,8 +155,8 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - gateway = GatewaysFixtures.create_gateway(account: account) - ResourcesFixtures.create_resource() + gateway = Fixtures.Gateways.create_gateway(account: account) + Fixtures.Resources.create_resource() assert list_resources_for_gateway(gateway, subject) == {:ok, []} end @@ -167,12 +165,12 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - group = GatewaysFixtures.create_group(account: account, subject: subject) - gateway = GatewaysFixtures.create_gateway(account: account, group: group) + group = Fixtures.Gateways.create_group(account: account, subject: subject) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: group.id}] + connections: [%{gateway_group_id: group.id}] ) |> delete_resource(subject) @@ -183,20 +181,20 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - group = GatewaysFixtures.create_group(account: account, subject: subject) - gateway = GatewaysFixtures.create_gateway(account: account, group: group) + group = Fixtures.Gateways.create_group(account: account, subject: subject) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: group.id}] + connections: [%{gateway_group_id: group.id}] ) - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: group.id}] + connections: [%{gateway_group_id: group.id}] ) - ResourcesFixtures.create_resource(account: account) + Fixtures.Resources.create_resource(account: account) assert {:ok, resources} = list_resources_for_gateway(gateway, subject) assert length(resources) == 2 @@ -206,15 +204,15 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - group = GatewaysFixtures.create_group(account: account, subject: subject) - gateway = GatewaysFixtures.create_gateway(account: account, group: group) + group = Fixtures.Gateways.create_group(account: account, subject: subject) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: group.id}] + connections: [%{gateway_group_id: group.id}] ) - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert list_resources_for_gateway(gateway, subject) == {:error, @@ -236,7 +234,7 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) assert count_resources_for_gateway(gateway, subject) == {:ok, 0} end @@ -245,15 +243,15 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - group = GatewaysFixtures.create_group(account: account, subject: subject) - gateway = GatewaysFixtures.create_gateway(account: account, group: group) + group = Fixtures.Gateways.create_group(account: account, subject: subject) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: group.id}] + connections: [%{gateway_group_id: group.id}] ) - ResourcesFixtures.create_resource(account: account) + Fixtures.Resources.create_resource(account: account) assert count_resources_for_gateway(gateway, subject) == {:ok, 1} end @@ -262,17 +260,17 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - group = GatewaysFixtures.create_group(account: account, subject: subject) - gateway = GatewaysFixtures.create_gateway(account: account, group: group) + group = Fixtures.Gateways.create_group(account: account, subject: subject) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: group.id}] + connections: [%{gateway_group_id: group.id}] ) - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: group.id}] + connections: [%{gateway_group_id: group.id}] ) |> delete_resource(subject) @@ -284,15 +282,15 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - group = GatewaysFixtures.create_group(account: account, subject: subject) - gateway = GatewaysFixtures.create_gateway(account: account, group: group) + group = Fixtures.Gateways.create_group(account: account, subject: subject) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: group.id}] + connections: [%{gateway_group_id: group.id}] ) - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert count_resources_for_gateway(gateway, subject) == {:error, @@ -373,9 +371,9 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, subject: subject, type: :cidr, @@ -391,14 +389,15 @@ defmodule Domain.ResourcesTest do assert {:error, changeset} = create_resource(attrs, subject) assert "can not overlap with other resource ranges" in errors_on(changeset).address - subject = AuthFixtures.create_subject() + # range is unique per account + subject = Fixtures.Auth.create_subject(actor: [type: :account_admin_user]) assert {:ok, _resource} = create_resource(attrs, subject) end test "returns error on duplicate name", %{account: account, subject: subject} do - gateway = GatewaysFixtures.create_gateway(account: account) - resource = ResourcesFixtures.create_resource(account: account, subject: subject) - address = ResourcesFixtures.resource_attrs().address + gateway = Fixtures.Gateways.create_gateway(account: account) + resource = Fixtures.Resources.create_resource(account: account, subject: subject) + address = Fixtures.Resources.resource_attrs().address attrs = %{ "name" => resource.name, @@ -412,10 +411,10 @@ defmodule Domain.ResourcesTest do end test "creates a dns resource", %{account: account, subject: subject} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) attrs = - ResourcesFixtures.resource_attrs( + Fixtures.Resources.resource_attrs( connections: [ %{gateway_group_id: gateway.group_id} ] @@ -447,11 +446,11 @@ defmodule Domain.ResourcesTest do end test "creates a cidr resource", %{account: account, subject: subject} do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) address_count = Repo.aggregate(Domain.Network.Address, :count) attrs = - ResourcesFixtures.resource_attrs( + Fixtures.Resources.resource_attrs( connections: [ %{gateway_group_id: gateway.group_id} ], @@ -493,10 +492,10 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) attrs = - ResourcesFixtures.resource_attrs( + Fixtures.Resources.resource_attrs( connections: [ %{gateway_group_id: gateway.group_id} ] @@ -515,11 +514,11 @@ defmodule Domain.ResourcesTest do assert %{address: resource.ipv6, type: :ipv6} in addresses assert_raise Ecto.ConstraintError, fn -> - NetworkFixtures.create_address(address: resource.ipv4, account: account) + Fixtures.Network.create_address(address: resource.ipv4, account: account) end assert_raise Ecto.ConstraintError, fn -> - NetworkFixtures.create_address(address: resource.ipv6, account: account) + Fixtures.Network.create_address(address: resource.ipv6, account: account) end end @@ -527,10 +526,10 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - gateway = GatewaysFixtures.create_gateway(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) attrs = - ResourcesFixtures.resource_attrs( + Fixtures.Resources.resource_attrs( connections: [ %{gateway_group_id: gateway.group_id} ] @@ -538,14 +537,14 @@ defmodule Domain.ResourcesTest do assert {:ok, resource} = create_resource(attrs, subject) - assert %Domain.Network.Address{} = NetworkFixtures.create_address(address: resource.ipv4) - assert %Domain.Network.Address{} = NetworkFixtures.create_address(address: resource.ipv6) + assert %Domain.Network.Address{} = Fixtures.Network.create_address(address: resource.ipv4) + assert %Domain.Network.Address{} = Fixtures.Network.create_address(address: resource.ipv6) end test "returns error when subject has no permission to create resources", %{ subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert create_resource(%{}, subject) == {:error, @@ -557,7 +556,7 @@ defmodule Domain.ResourcesTest do describe "update_resource/3" do setup context do resource = - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: context.account, subject: context.subject ) @@ -593,14 +592,14 @@ defmodule Domain.ResourcesTest do end test "allows to update connections", %{account: account, resource: resource, subject: subject} do - gateway1 = GatewaysFixtures.create_gateway(account: account) + gateway1 = Fixtures.Gateways.create_gateway(account: account) attrs = %{"connections" => [%{gateway_group_id: gateway1.group_id}]} assert {:ok, resource} = update_resource(resource, attrs, subject) gateway_group_ids = Enum.map(resource.connections, & &1.gateway_group_id) assert gateway_group_ids == [gateway1.group_id] - gateway2 = GatewaysFixtures.create_gateway(account: account) + gateway2 = Fixtures.Gateways.create_gateway(account: account) attrs = %{ "connections" => [ @@ -638,7 +637,7 @@ defmodule Domain.ResourcesTest do resource: resource, subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert update_resource(resource, %{}, subject) == {:error, @@ -650,7 +649,7 @@ defmodule Domain.ResourcesTest do describe "delete_resource/2" do setup context do resource = - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: context.account, subject: context.subject ) @@ -676,7 +675,7 @@ defmodule Domain.ResourcesTest do resource: resource, subject: subject } do - subject = AuthFixtures.remove_permissions(subject) + subject = Fixtures.Auth.remove_permissions(subject) assert delete_resource(resource, subject) == {:error, @@ -690,21 +689,21 @@ defmodule Domain.ResourcesTest do account: account, subject: subject } do - group = GatewaysFixtures.create_group(account: account, subject: subject) - gateway = GatewaysFixtures.create_gateway(account: account, group: group) + group = Fixtures.Gateways.create_group(account: account, subject: subject) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) resource = - ResourcesFixtures.create_resource( + Fixtures.Resources.create_resource( account: account, - gateway_groups: [%{gateway_group_id: group.id}] + connections: [%{gateway_group_id: group.id}] ) assert connected?(resource, gateway) end test "raises resource and gateway don't belong to the same account" do - gateway = GatewaysFixtures.create_gateway() - resource = ResourcesFixtures.create_resource() + gateway = Fixtures.Gateways.create_gateway() + resource = Fixtures.Resources.create_resource() assert_raise FunctionClauseError, fn -> connected?(resource, gateway) @@ -712,8 +711,8 @@ defmodule Domain.ResourcesTest do end test "returns false when resource has no connection to a gateway", %{account: account} do - gateway = GatewaysFixtures.create_gateway(account: account) - resource = ResourcesFixtures.create_resource(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account) + resource = Fixtures.Resources.create_resource(account: account) refute connected?(resource, gateway) end diff --git a/elixir/apps/domain/test/domain/telemetry_test.exs b/elixir/apps/domain/test/domain/telemetry_test.exs deleted file mode 100644 index 866cea0cf..000000000 --- a/elixir/apps/domain/test/domain/telemetry_test.exs +++ /dev/null @@ -1,150 +0,0 @@ -# defmodule Domain.TelemetryTest do -# use Domain.DataCase, async: true -# # import Domain.TestHelpers -# # alias Domain.Telemetry -# # alias Domain.MFAFixtures - -# # describe "user" do -# # setup :create_user - -# # test "count" do -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:user_count] == 1 -# # end - -# # test "count mfa", %{user: user} do -# # {:ok, [user: other_user]} = create_user(%{}) -# # MFAFixtures.create_totp_method(user: user) -# # MFAFixtures.create_totp_method(user: other_user) -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:users_with_mfa] == 2 -# # assert ping_data[:users_with_mfa_totp] == 2 -# # end -# # end - -# # describe "device" do -# # setup [:create_devices, :create_other_user_device] - -# # test "count" do -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:device_count] == 6 -# # end - -# # test "max count for users" do -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:max_devices_for_users] == 5 -# # end -# # end - -# # describe "auth" do -# # test "count openid providers" do -# # Domain.ConfigFixtures.start_openid_providers([ -# # "google", -# # "okta", -# # "auth0", -# # "azure", -# # "onelogin", -# # "keycloak", -# # "vault" -# # ]) - -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:openid_providers] == 7 -# # end - -# # test "disable vpn on oidc error enabled" do -# # Domain.Config.put_config!(:disable_vpn_on_oidc_error, true) - -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:disable_vpn_on_oidc_error] -# # end - -# # test "disable vpn on oidc error disabled" do -# # Domain.Config.put_config!(:disable_vpn_on_oidc_error, false) - -# # ping_data = Telemetry.ping_data() - -# # refute ping_data[:disable_vpn_on_oidc_error] -# # end - -# # test "local authentication enabled" do -# # Domain.Config.put_config!(:local_auth_enabled, true) - -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:local_authentication] -# # end - -# # test "local authentication disabled" do -# # Domain.Config.put_config!(:local_auth_enabled, false) - -# # ping_data = Telemetry.ping_data() - -# # refute ping_data[:local_authentication] -# # end - -# # test "unprivileged device management enabled" do -# # Domain.Config.put_config!(:allow_unprivileged_device_management, true) - -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:unprivileged_device_management] -# # end - -# # test "unprivileged device configuration enabled" do -# # Domain.Config.put_config!(:allow_unprivileged_device_configuration, true) - -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:unprivileged_device_configuration] -# # end - -# # test "unprivileged device configuration disabled" do -# # Domain.Config.put_config!(:allow_unprivileged_device_configuration, false) - -# # ping_data = Telemetry.ping_data() - -# # refute ping_data[:unprivileged_device_configuration] -# # end -# # end - -# # describe "database" do -# # test "local hostname" do -# # Domain.Config.put_env_override(:domain, Domain.Repo, hostname: "localhost") - -# # ping_data = Telemetry.ping_data() - -# # refute ping_data[:external_database] -# # end - -# # test "local url" do -# # Domain.Config.put_env_override(:domain, Domain.Repo, url: "postgres://127.0.0.1") - -# # ping_data = Telemetry.ping_data() - -# # refute ping_data[:external_database] -# # end - -# # test "external hostname" do -# # Domain.Config.put_env_override(:domain, Domain.Repo, hostname: "firezone.dev") - -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:external_database] -# # end - -# # test "external url" do -# # Domain.Config.put_env_override(:domain, Domain.Repo, url: "postgres://firezone.dev") - -# # ping_data = Telemetry.ping_data() - -# # assert ping_data[:external_database] -# # end -# # end -# end diff --git a/elixir/apps/domain/test/support/data_case.ex b/elixir/apps/domain/test/support/data_case.ex index b467c17d6..c80f97923 100644 --- a/elixir/apps/domain/test/support/data_case.ex +++ b/elixir/apps/domain/test/support/data_case.ex @@ -23,6 +23,8 @@ defmodule Domain.DataCase do import Domain.DataCase alias Domain.Repo + alias Domain.Fixtures + alias Domain.Mocks end end diff --git a/elixir/apps/domain/test/support/fixture.ex b/elixir/apps/domain/test/support/fixture.ex new file mode 100644 index 000000000..c17bc3765 --- /dev/null +++ b/elixir/apps/domain/test/support/fixture.ex @@ -0,0 +1,65 @@ +defmodule Domain.Fixture do + alias Domain.Repo + + defmacro __using__(_opts) do + quote do + import Domain.Fixture + alias Domain.Repo + alias Domain.Fixtures + end + end + + def pop_assoc_fixture_id(attrs, key, callback) do + case Map.fetch(attrs, :"#{key}_id") do + {:ok, id} when not is_nil(id) -> + {id, attrs} + + _other -> + {assoc, attrs} = pop_assoc_fixture(attrs, key, callback) + {assoc.id, attrs} + end + end + + def pop_assoc_fixture(attrs, key, callback) do + case Map.pop(attrs, key, %{}) do + {%{__struct__: _struct} = assoc_struct, attrs} -> + {assoc_struct, attrs} + + {assoc_attrs, attrs} -> + {apply_assoc_fixture(callback, assoc_attrs), attrs} + end + end + + defp apply_assoc_fixture(callback, _attrs) when is_function(callback, 0), do: callback.() + defp apply_assoc_fixture(callback, attrs) when is_function(callback, 1), do: callback.(attrs) + + def update!(schema, changes) do + schema + |> Ecto.Changeset.change(Enum.into(changes, %{})) + |> Repo.update!() + end + + def unique_integer do + System.unique_integer([:positive]) + end + + def unique_ipv4 do + number = unique_integer() + <> = <> + {a, b, c, d} + end + + def unique_ipv6 do + number = unique_integer() + + <> = <> + + {a, b, c, d, e, f, g, h} + end + + def unique_public_key do + :crypto.strong_rand_bytes(32) + |> Base.encode64() + end +end diff --git a/elixir/apps/domain/test/support/fixtures/accounts_fixtures.ex b/elixir/apps/domain/test/support/fixtures/accounts.ex similarity index 64% rename from elixir/apps/domain/test/support/fixtures/accounts_fixtures.ex rename to elixir/apps/domain/test/support/fixtures/accounts.ex index c43e189b7..7231d1070 100644 --- a/elixir/apps/domain/test/support/fixtures/accounts_fixtures.ex +++ b/elixir/apps/domain/test/support/fixtures/accounts.ex @@ -1,9 +1,10 @@ -defmodule Domain.AccountsFixtures do +defmodule Domain.Fixtures.Accounts do + use Domain.Fixture alias Domain.Accounts def account_attrs(attrs \\ %{}) do Enum.into(attrs, %{ - name: "acc-#{counter()}" + name: "acc-#{unique_integer()}" }) end @@ -12,8 +13,4 @@ defmodule Domain.AccountsFixtures do {:ok, account} = Accounts.create_account(attrs) account end - - defp counter do - System.unique_integer([:positive]) - end end diff --git a/elixir/apps/domain/test/support/fixtures/actors.ex b/elixir/apps/domain/test/support/fixtures/actors.ex new file mode 100644 index 000000000..eee597f48 --- /dev/null +++ b/elixir/apps/domain/test/support/fixtures/actors.ex @@ -0,0 +1,136 @@ +defmodule Domain.Fixtures.Actors do + use Domain.Fixture + alias Domain.Actors + + def group_attrs(attrs \\ %{}) do + Enum.into(attrs, %{ + name: "group-#{unique_integer()}" + }) + end + + def create_group(attrs \\ %{}) do + attrs = group_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {provider, attrs} = + Map.pop(attrs, :provider) + + {provider_identifier, attrs} = + Map.pop_lazy(attrs, :provider_identifier, fn -> + if provider do + Fixtures.Auth.random_provider_identifier(provider) + end + end) + + {subject, attrs} = + pop_assoc_fixture(attrs, :subject, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account, actor: [type: :account_admin_user]}) + |> Fixtures.Auth.create_subject() + end) + + {:ok, group} = + attrs + |> Map.put(:provider_identifier, provider_identifier) + |> Actors.create_group(subject) + + if provider do + update!(group, provider_id: provider.id, provider_identifier: provider_identifier) + else + group + end + end + + def delete_group(group) do + group = Repo.preload(group, :account) + + subject = + Fixtures.Auth.create_subject( + account: group.account, + actor: [type: :account_admin_user] + ) + + {:ok, group} = Actors.delete_group(group, subject) + group + end + + def actor_attrs(attrs \\ %{}) do + first_name = Enum.random(~w[Wade Dave Seth Riley Gilbert Jorge Dan Brian Roberto Ramon Juan]) + last_name = Enum.random(~w[Robyn Traci Desiree Jon Bob Karl Joe Alberta Lynda Cara Brandi B]) + + Enum.into(attrs, %{ + name: "#{first_name} #{last_name}", + type: :account_user + }) + end + + def create_actor(attrs \\ %{}) do + attrs = actor_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {provider, attrs} = + pop_assoc_fixture(attrs, :provider, fn assoc_attrs -> + {provider, _bypass} = + assoc_attrs + |> Enum.into(%{account: account}) + |> Fixtures.Auth.start_and_create_openid_connect_provider() + + provider + end) + + Actors.Actor.Changeset.create(provider.account_id, attrs) + |> Repo.insert!() + end + + def create_membership(attrs \\ %{}) do + attrs = Enum.into(attrs, %{}) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {provider, attrs} = + Map.pop(attrs, :provider) + + {group_id, attrs} = + pop_assoc_fixture_id(attrs, :group, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account, provider: provider}) + |> create_group() + end) + + {actor_id, _attrs} = + pop_assoc_fixture_id(attrs, :actor, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account}) + |> create_actor() + end) + + Actors.Membership.Changeset.changeset(account.id, %Actors.Membership{}, %{ + group_id: group_id, + actor_id: actor_id + }) + |> Repo.insert!() + end + + def update(actor, updates) do + update!(actor, updates) + end + + def disable(actor) do + update!(actor, %{disabled_at: DateTime.utc_now()}) + end + + def delete(actor) do + update!(actor, %{deleted_at: DateTime.utc_now()}) + end +end diff --git a/elixir/apps/domain/test/support/fixtures/actors_fixtures.ex b/elixir/apps/domain/test/support/fixtures/actors_fixtures.ex deleted file mode 100644 index 0c518639e..000000000 --- a/elixir/apps/domain/test/support/fixtures/actors_fixtures.ex +++ /dev/null @@ -1,131 +0,0 @@ -defmodule Domain.ActorsFixtures do - alias Domain.Repo - alias Domain.Actors - alias Domain.{AccountsFixtures, AuthFixtures} - - def group_attrs(attrs \\ %{}) do - Enum.into(attrs, %{ - name: "group-#{counter()}" - }) - end - - def create_group(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {provider, attrs} = - Map.pop(attrs, :provider) - - {provider_identifier, attrs} = - Map.pop_lazy(attrs, :provider_identifier, fn -> - Ecto.UUID.generate() - end) - - {subject, attrs} = - Map.pop_lazy(attrs, :subject, fn -> - actor = create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - AuthFixtures.create_subject(identity) - end) - - attrs = group_attrs(attrs) - - {:ok, group} = Actors.create_group(attrs, subject) - - if provider do - group - |> Ecto.Changeset.change(provider_id: provider.id, provider_identifier: provider_identifier) - |> Repo.update!() - else - group - end - end - - # def create_provider_group(attrs \\ %{}) do - # attrs = Enum.into(attrs, %{}) - - # {account, attrs} = - # Map.pop_lazy(attrs, :account, fn -> - # AccountsFixtures.create_account() - # end) - - # {provider_identifier, attrs} = - # Map.pop_lazy(attrs, :provider_identifier, fn -> - # Ecto.UUID.generate() - # end) - - # {provider, attrs} = - # Map.pop_lazy(attrs, :account, fn -> - # AccountsFixtures.create_account() - # end) - - # attrs = group_attrs(attrs) - - # {:ok, group} = Actors.upsert_provider_group(provider, provider_identifier, attrs) - # group - # end - - def delete_group(group) do - group = Repo.preload(group, :account) - actor = create_actor(type: :account_admin_user, account: group.account) - identity = AuthFixtures.create_identity(account: group.account, actor: actor) - subject = AuthFixtures.create_subject(identity) - {:ok, group} = Actors.delete_group(group, subject) - group - end - - def actor_attrs(attrs \\ %{}) do - first_name = Enum.random(~w[Wade Dave Seth Riley Gilbert Jorge Dan Brian Roberto Ramon]) - last_name = Enum.random(~w[Robyn Traci Desiree Jon Bob Karl Joe Alberta Lynda Cara Brandi]) - - Enum.into(attrs, %{ - name: "#{first_name} #{last_name}", - type: :account_user - }) - end - - def create_actor(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {provider, attrs} = - Map.pop_lazy(attrs, :provider, fn -> - {provider, _bypass} = - AuthFixtures.start_openid_providers(["google"]) - |> AuthFixtures.create_openid_connect_provider(account: account) - - provider - end) - - attrs = actor_attrs(attrs) - - Actors.Actor.Changeset.create_changeset(provider.account_id, attrs) - |> Repo.insert!() - end - - def update(actor, updates) do - actor - |> Ecto.Changeset.change(Map.new(updates)) - |> Repo.update!() - end - - def disable(actor) do - update(actor, %{disabled_at: DateTime.utc_now()}) - end - - def delete(actor) do - update(actor, %{deleted_at: DateTime.utc_now()}) - end - - defp counter do - System.unique_integer([:positive]) - end -end diff --git a/elixir/apps/domain/test/support/fixtures/auth.ex b/elixir/apps/domain/test/support/fixtures/auth.ex new file mode 100644 index 000000000..561071551 --- /dev/null +++ b/elixir/apps/domain/test/support/fixtures/auth.ex @@ -0,0 +1,353 @@ +defmodule Domain.Fixtures.Auth do + use Domain.Fixture + alias Domain.Auth + + def user_password, do: "Hello w0rld!" + def remote_ip, do: {100, 64, 100, 58} + def user_agent, do: "iOS/12.5 (iPhone) connlib/0.7.412" + def email, do: "user-#{unique_integer()}@example.com" + + def random_provider_identifier(%Domain.Auth.Provider{adapter: :email, name: name}) do + "user-#{unique_integer()}@#{String.downcase(name)}.com" + end + + def random_provider_identifier(%Domain.Auth.Provider{adapter: :openid_connect}) do + Ecto.UUID.generate() + end + + def random_provider_identifier(%Domain.Auth.Provider{adapter: :google_workspace}) do + Ecto.UUID.generate() + end + + def random_provider_identifier(%Domain.Auth.Provider{adapter: :token}) do + Ecto.UUID.generate() + end + + def random_provider_identifier(%Domain.Auth.Provider{adapter: :userpass, name: name}) do + "user-#{unique_integer()}@#{String.downcase(name)}.com" + end + + def provider_attrs(attrs \\ %{}) do + Enum.into(attrs, %{ + name: "provider-#{unique_integer()}", + adapter: :email, + adapter_config: %{}, + created_by: :system, + provisioner: :manual + }) + end + + def openid_connect_adapter_config(overrides \\ %{}) do + for {k, v} <- overrides, + into: %{ + "discovery_document_uri" => + "https://firezone.example.com/.well-known/openid-configuration", + "client_id" => "client-id-#{unique_integer()}", + "client_secret" => "client-secret-#{unique_integer()}", + "response_type" => "code", + "scope" => "openid email profile" + } do + {to_string(k), v} + end + end + + def create_email_provider(attrs \\ %{}) do + attrs = provider_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {:ok, provider} = Auth.create_provider(account, attrs) + provider + end + + def start_and_create_openid_connect_provider(attrs \\ %{}) do + bypass = Domain.Mocks.OpenIDConnect.discovery_document_server() + + adapter_config = + openid_connect_adapter_config( + discovery_document_uri: "http://localhost:#{bypass.port}/.well-known/openid-configuration" + ) + + provider = + attrs + |> Enum.into(%{adapter_config: adapter_config}) + |> create_openid_connect_provider() + + {provider, bypass} + end + + def start_and_create_google_workspace_provider(attrs \\ %{}) do + bypass = Domain.Mocks.OpenIDConnect.discovery_document_server() + + adapter_config = + openid_connect_adapter_config( + discovery_document_uri: "http://localhost:#{bypass.port}/.well-known/openid-configuration" + ) + + provider = + attrs + |> Enum.into(%{adapter_config: adapter_config}) + |> create_google_workspace_provider() + + {provider, bypass} + end + + def create_openid_connect_provider(attrs \\ %{}) do + attrs = + %{ + adapter: :openid_connect, + provisioner: :just_in_time + } + |> Map.merge(Enum.into(attrs, %{})) + |> provider_attrs() + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {:ok, provider} = Auth.create_provider(account, attrs) + + provider = + provider + |> Ecto.Changeset.change( + disabled_at: nil, + adapter_state: %{} + ) + |> Repo.update!() + + provider + end + + def create_google_workspace_provider(attrs \\ %{}) do + attrs = + %{ + adapter: :google_workspace, + provisioner: :custom + } + |> Map.merge(Enum.into(attrs, %{})) + |> provider_attrs() + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {:ok, provider} = Auth.create_provider(account, attrs) + + update!(provider, + disabled_at: nil, + adapter_state: %{ + "access_token" => "OIDC_ACCESS_TOKEN", + "refresh_token" => "OIDC_REFRESH_TOKEN", + "expires_at" => DateTime.utc_now() |> DateTime.add(1, :day), + "claims" => "openid email profile offline_access" + } + ) + end + + def create_userpass_provider(attrs \\ %{}) do + attrs = + attrs + |> Enum.into(%{adapter: :userpass}) + |> provider_attrs() + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {:ok, provider} = Auth.create_provider(account, attrs) + provider + end + + def create_token_provider(attrs \\ %{}) do + attrs = + attrs + |> Enum.into(%{adapter: :token}) + |> provider_attrs() + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {:ok, provider} = Auth.create_provider(account, attrs) + provider + end + + def disable_provider(provider) do + provider = Repo.preload(provider, :account) + + subject = + Fixtures.Auth.create_subject( + account: provider.account, + actor: [type: :account_admin_user] + ) + + {:ok, group} = Auth.disable_provider(provider, subject) + group + end + + def delete_provider(provider) do + update!(provider, deleted_at: DateTime.utc_now()) + end + + def identity_attrs(attrs \\ %{}) do + Enum.into(attrs, %{ + provider_virtual_state: %{} + }) + end + + def create_identity(attrs \\ %{}) do + attrs = identity_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {provider, attrs} = + pop_assoc_fixture(attrs, :provider, fn assoc_attrs -> + {provider, _bypass} = + assoc_attrs + |> Enum.into(%{account: account}) + |> start_and_create_openid_connect_provider() + + provider + end) + + {provider_identifier, attrs} = + Map.pop_lazy(attrs, :provider_identifier, fn -> + random_provider_identifier(provider) + end) + + {provider_state, attrs} = + Map.pop(attrs, :provider_state) + + {actor, attrs} = + pop_assoc_fixture(attrs, :actor, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{ + account: account, + provider: provider, + provider_identifier: provider_identifier + }) + |> Fixtures.Actors.create_actor() + end) + + attrs = Map.put(attrs, :provider_identifier, provider_identifier) + + {:ok, identity} = Auth.upsert_identity(actor, provider, attrs) + + if provider_state do + identity + |> Ecto.Changeset.change(provider_state: provider_state) + |> Repo.update!() + else + identity + end + end + + def delete_identity(identity) do + update!(identity, deleted_at: DateTime.utc_now()) + end + + def create_subject(attrs \\ %{}) do + attrs = Enum.into(attrs, %{}) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + relation = attrs[:provider] || attrs[:actor] || attrs[:identity] + + if not is_nil(relation) and is_struct(relation) do + Repo.get!(Domain.Accounts.Account, relation.account_id) + else + Fixtures.Accounts.create_account(assoc_attrs) + end + end) + + {provider, attrs} = + pop_assoc_fixture(attrs, :provider, fn assoc_attrs -> + relation = attrs[:identity] + + if not is_nil(relation) and is_struct(relation) do + Repo.get!(Domain.Auth.Provider, relation.provider_id) + else + {provider, _bypass} = + assoc_attrs + |> Enum.into(%{account: account}) + |> start_and_create_openid_connect_provider() + + provider + end + end) + + {provider_identifier, attrs} = + Map.pop_lazy(attrs, :provider_identifier, fn -> + random_provider_identifier(provider) + end) + + {actor, attrs} = + pop_assoc_fixture(attrs, :actor, fn assoc_attrs -> + relation = attrs[:identity] + + if not is_nil(relation) and is_struct(relation) do + Repo.get!(Domain.Actors.Actor, relation.actor_id) + else + assoc_attrs + |> Enum.into(%{ + type: :account_admin_user, + account: account, + provider: provider, + provider_identifier: provider_identifier + }) + |> Fixtures.Actors.create_actor() + end + end) + + {identity, attrs} = + pop_assoc_fixture(attrs, :identity, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{ + actor: actor, + account: account, + provider: provider, + provider_identifier: provider_identifier + }) + |> create_identity() + end) + + {expires_at, attrs} = + Map.pop_lazy(attrs, :expires_at, fn -> + DateTime.utc_now() |> DateTime.add(60, :second) + end) + + {user_agent, attrs} = + Map.pop_lazy(attrs, :user_agent, fn -> + user_agent() + end) + + {remote_ip, _attrs} = + Map.pop_lazy(attrs, :remote_ip, fn -> + remote_ip() + end) + + Auth.build_subject(identity, expires_at, user_agent, remote_ip) + end + + def remove_permissions(%Auth.Subject{} = subject) do + %{subject | permissions: MapSet.new()} + end + + def set_permissions(%Auth.Subject{} = subject, permissions) do + %{subject | permissions: MapSet.new(permissions)} + end + + def add_permission(%Auth.Subject{} = subject, permission) do + %{subject | permissions: MapSet.put(subject.permissions, permission)} + end +end diff --git a/elixir/apps/domain/test/support/fixtures/auth_fixtures.ex b/elixir/apps/domain/test/support/fixtures/auth_fixtures.ex deleted file mode 100644 index b0b1b30f4..000000000 --- a/elixir/apps/domain/test/support/fixtures/auth_fixtures.ex +++ /dev/null @@ -1,545 +0,0 @@ -defmodule Domain.AuthFixtures do - alias Domain.Repo - alias Domain.Auth - alias Domain.AccountsFixtures - alias Domain.ActorsFixtures - - def user_password, do: "Hello w0rld!" - def remote_ip, do: {100, 64, 100, 58} - def user_agent, do: "iOS/12.5 (iPhone) connlib/0.7.412" - def email, do: "user-#{counter()}@example.com" - - def random_provider_identifier(%Domain.Auth.Provider{adapter: :email, name: name}) do - "user-#{counter()}@#{String.downcase(name)}.com" - end - - def random_provider_identifier(%Domain.Auth.Provider{adapter: :openid_connect}) do - Ecto.UUID.generate() - end - - def random_provider_identifier(%Domain.Auth.Provider{adapter: :google_workspace}) do - Ecto.UUID.generate() - end - - def random_provider_identifier(%Domain.Auth.Provider{adapter: :token}) do - Ecto.UUID.generate() - end - - def random_provider_identifier(%Domain.Auth.Provider{adapter: :userpass, name: name}) do - "user-#{counter()}@#{String.downcase(name)}.com" - end - - def provider_attrs(attrs \\ %{}) do - Enum.into(attrs, %{ - name: "provider-#{counter()}", - adapter: :email, - adapter_config: %{}, - created_by: :system, - provisioner: :manual - }) - end - - def create_email_provider(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - attrs = provider_attrs(attrs) - - {:ok, provider} = Auth.create_provider(account, attrs) - provider - end - - def create_openid_connect_provider({bypass, [provider_attrs]}, attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - attrs = - %{ - adapter: :openid_connect, - adapter_config: provider_attrs, - provisioner: :just_in_time - } - |> Map.merge(attrs) - |> provider_attrs() - - {:ok, provider} = Auth.create_provider(account, attrs) - - provider = - provider - |> Ecto.Changeset.change( - disabled_at: nil, - adapter_state: %{} - ) - |> Repo.update!() - - {provider, bypass} - end - - def create_google_workspace_provider({bypass, [provider_attrs]}, attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - attrs = - %{ - adapter: :google_workspace, - adapter_config: provider_attrs, - provisioner: :custom - } - |> Map.merge(attrs) - |> provider_attrs() - - {:ok, provider} = Auth.create_provider(account, attrs) - - provider = - provider - |> Ecto.Changeset.change( - disabled_at: nil, - adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", - "refresh_token" => "OIDC_REFRESH_TOKEN", - "expires_at" => DateTime.utc_now() |> DateTime.add(1, :day), - "claims" => "openid email profile offline_access" - } - ) - |> Repo.update!() - - {provider, bypass} - end - - def create_userpass_provider(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, _attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - attrs = provider_attrs(adapter: :userpass) - - {:ok, provider} = Auth.create_provider(account, attrs) - provider - end - - def create_token_provider(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, _attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - attrs = provider_attrs(adapter: :token) - - {:ok, provider} = Auth.create_provider(account, attrs) - provider - end - - def disable_provider(provider) do - provider = Repo.preload(provider, :account) - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: provider.account) - identity = create_identity(account: provider.account, actor: actor) - subject = create_subject(identity) - {:ok, group} = Auth.disable_provider(provider, subject) - group - end - - def create_identity(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {provider, attrs} = - Map.pop_lazy(attrs, :provider, fn -> - {provider, _bypass} = - start_openid_providers(["google"]) - |> create_openid_connect_provider(account: account) - - provider - end) - - {provider_identifier, attrs} = - Map.pop_lazy(attrs, :provider_identifier, fn -> - random_provider_identifier(provider) - end) - - {actor_default_type, attrs} = - Map.pop(attrs, :actor_default_type, :account_user) - - {actor, _attrs} = - Map.pop_lazy(attrs, :actor, fn -> - ActorsFixtures.create_actor( - type: actor_default_type, - account: account, - provider: provider, - provider_identifier: provider_identifier - ) - end) - - {provider_virtual_state, attrs} = - Map.pop_lazy(attrs, :provider_virtual_state, fn -> - %{} - end) - - {:ok, identity} = - Auth.upsert_identity(actor, provider, provider_identifier, provider_virtual_state) - - if state = Map.get(attrs, :provider_state) do - identity - |> Ecto.Changeset.change(provider_state: state) - |> Repo.update!() - else - identity - end - end - - def delete_identity(identity) do - identity - |> Ecto.Changeset.change(deleted_at: DateTime.utc_now()) - |> Repo.update!() - end - - def create_subject do - account = AccountsFixtures.create_account() - - {provider, _bypass} = - start_openid_providers(["google"]) - |> create_openid_connect_provider(account: account) - - actor = - ActorsFixtures.create_actor( - type: :account_admin_user, - account: account, - provider: provider - ) - - identity = create_identity(actor: actor, account: account, provider: provider) - create_subject(identity) - end - - def create_subject(%Auth.Identity{} = identity) do - identity = Repo.preload(identity, [:account, :actor]) - - %Auth.Subject{ - identity: identity, - actor: identity.actor, - permissions: Auth.Roles.build(identity.actor.type).permissions, - account: identity.account, - expires_at: DateTime.utc_now() |> DateTime.add(60, :second), - context: %Auth.Context{remote_ip: remote_ip(), user_agent: user_agent()} - } - end - - def remove_permissions(%Auth.Subject{} = subject) do - %{subject | permissions: MapSet.new()} - end - - def set_permissions(%Auth.Subject{} = subject, permissions) do - %{subject | permissions: MapSet.new(permissions)} - end - - def add_permission(%Auth.Subject{} = subject, permission) do - %{subject | permissions: MapSet.put(subject.permissions, permission)} - end - - def start_openid_providers(provider_names, overrides \\ %{}) do - {bypass, discovery_document_url} = discovery_document_server() - - openid_connect_providers_attrs = - discovery_document_url - |> openid_connect_providers_attrs() - |> Enum.filter(fn {name, _config} -> - name in provider_names - end) - |> Enum.map(fn {_name, config} -> - config - |> Enum.into(%{}) - |> Map.merge(overrides) - end) - - {bypass, openid_connect_providers_attrs} - end - - def openid_connect_provider_attrs(overrides \\ %{}) do - Enum.into(overrides, %{ - "discovery_document_uri" => "https://firezone.example.com/.well-known/openid-configuration", - "client_id" => "google-client-id-#{counter()}", - "client_secret" => "google-client-secret", - "response_type" => "code", - "scope" => "openid email profile" - }) - end - - defp openid_connect_providers_attrs(discovery_document_url) do - %{ - "google" => %{ - "discovery_document_uri" => discovery_document_url, - "client_id" => "google-client-id-#{counter()}", - "client_secret" => "google-client-secret", - "response_type" => "code", - "scope" => "openid email profile" - }, - "okta" => %{ - "discovery_document_uri" => discovery_document_url, - "client_id" => "okta-client-id-#{counter()}", - "client_secret" => "okta-client-secret", - "response_type" => "code", - "scope" => "openid email profile offline_access" - }, - "auth0" => %{ - "discovery_document_uri" => discovery_document_url, - "client_id" => "auth0-client-id-#{counter()}", - "client_secret" => "auth0-client-secret", - "response_type" => "code", - "scope" => "openid email profile" - }, - "azure" => %{ - "discovery_document_uri" => discovery_document_url, - "client_id" => "azure-client-id-#{counter()}", - "client_secret" => "azure-client-secret", - "response_type" => "code", - "scope" => "openid email profile offline_access" - }, - "onelogin" => %{ - "discovery_document_uri" => discovery_document_url, - "client_id" => "onelogin-client-id-#{counter()}", - "client_secret" => "onelogin-client-secret", - "response_type" => "code", - "scope" => "openid email profile offline_access" - }, - "keycloak" => %{ - "discovery_document_uri" => discovery_document_url, - "client_id" => "keycloak-client-id-#{counter()}", - "client_secret" => "keycloak-client-secret", - "response_type" => "code", - "scope" => "openid email profile offline_access" - }, - "vault" => %{ - "discovery_document_uri" => discovery_document_url, - "client_id" => "vault-client-id-#{counter()}", - "client_secret" => "vault-client-secret", - "response_type" => "code", - "scope" => "openid email profile offline_access" - } - } - end - - def jwks_attrs do - %{ - "alg" => "RS256", - "d" => - "X8TM24Zqbiha9geYYk_vZpANu16IadJLJLJ7ucTc3JaMbK8NCYNcHMoXKnNYPFxmq-UWAEIwh-2" <> - "txOiOxuChVrblpfyE4SBJio1T0AUcCwmm8U6G-CsSHMMzWTt2dMTnArHjdyAIgOVRW5SVzhTT" <> - "taf4JY-47S-fbcJ7g0hmBbVih5i1sE2fad4I4qFHT-YFU_pnUHbteR6GQuRW4r03Eon8Aje6a" <> - "l2AxcYnfF8_cSOIOpkDgGavTtGYhhZPi2jZ7kPm6QGkNW5CyfEq5PGB6JOihw-XIFiiMzYgx0" <> - "52rnzoqALoLheXrI0By4kgHSmcqOOmq7aiOff45rlSbpsR", - "e" => "AQAB", - "kid" => "example@firezone.dev", - "kty" => "RSA", - "n" => - "qlKll8no4lPYXNSuTTnacpFHiXwPOv_htCYvIXmiR7CWhiiOHQqj7KWXIW7TGxyoLVIyeRM4mwv" <> - "kLI-UgsSMYdEKTT0j7Ydjrr0zCunPu5Gxr2yOmcRaszAzGxJL5DwpA0V40RqMlm5OuwdqS4To" <> - "_p9LlLxzMF6RZe1OqslV5RZ4Y8FmrWq6BV98eIziEHL0IKdsAIrrOYkkcLDdQeMNuTp_yNB8X" <> - "l2TdWSdsbRomrs2dCtCqZcXTsy2EXDceHvYhgAB33nh_w17WLrZQwMM-7kJk36Kk54jZd7i80" <> - "AJf_s_plXn1mEh-L5IAL1vg3a9EOMFUl-lPiGqc3td_ykH", - "use" => "sig" - } - end - - def expect_refresh_token(bypass, attrs \\ %{}) do - test_pid = self() - - Bypass.expect(bypass, "POST", "/oauth/token", fn conn -> - conn = fetch_conn_params(conn) - send(test_pid, {:request, conn}) - Plug.Conn.resp(conn, 200, Jason.encode!(attrs)) - end) - end - - def expect_refresh_token_failure(bypass, attrs \\ %{}) do - test_pid = self() - - Bypass.expect(bypass, "POST", "/oauth/token", fn conn -> - conn = fetch_conn_params(conn) - send(test_pid, {:request, conn}) - Plug.Conn.resp(conn, 401, Jason.encode!(attrs)) - end) - end - - def expect_userinfo(bypass, attrs \\ %{}) do - test_pid = self() - - Bypass.expect(bypass, "GET", "/userinfo", fn conn -> - attrs = - Map.merge( - %{ - "sub" => "353690423699814251281", - "name" => "Ada Lovelace", - "given_name" => "Ada", - "family_name" => "Lovelace", - "picture" => - "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg", - "email" => "ada@example.com", - "email_verified" => true, - "locale" => "en" - }, - attrs - ) - - conn = fetch_conn_params(conn) - send(test_pid, {:request, conn}) - Plug.Conn.resp(conn, 200, Jason.encode!(attrs)) - end) - end - - def discovery_document_server do - bypass = Bypass.open() - endpoint = "http://localhost:#{bypass.port}" - test_pid = self() - - Bypass.stub(bypass, "GET", "/.well-known/jwks.json", fn conn -> - attrs = %{"keys" => [jwks_attrs()]} - Plug.Conn.resp(conn, 200, Jason.encode!(attrs)) - end) - - Bypass.stub(bypass, "GET", "/.well-known/openid-configuration", fn conn -> - conn = fetch_conn_params(conn) - send(test_pid, {:request, conn}) - - attrs = %{ - "issuer" => "#{endpoint}/", - "authorization_endpoint" => "#{endpoint}/authorize", - "token_endpoint" => "#{endpoint}/oauth/token", - "device_authorization_endpoint" => "#{endpoint}/oauth/device/code", - "userinfo_endpoint" => "#{endpoint}/userinfo", - "mfa_challenge_endpoint" => "#{endpoint}/mfa/challenge", - "jwks_uri" => "#{endpoint}/.well-known/jwks.json", - "registration_endpoint" => "#{endpoint}/oidc/register", - "revocation_endpoint" => "#{endpoint}/oauth/revoke", - "end_session_endpoint" => "https://example.com", - "scopes_supported" => [ - "openid", - "profile", - "offline_access", - "name", - "given_name", - "family_name", - "nickname", - "email", - "email_verified", - "picture", - "created_at", - "identities", - "phone", - "address" - ], - "response_types_supported" => [ - "code", - "token", - "id_token", - "code token", - "code id_token", - "token id_token", - "code token id_token" - ], - "code_challenge_methods_supported" => [ - "S256", - "plain" - ], - "response_modes_supported" => [ - "query", - "fragment", - "form_post" - ], - "subject_types_supported" => [ - "public" - ], - "id_token_signing_alg_values_supported" => [ - "HS256", - "RS256" - ], - "token_endpoint_auth_methods_supported" => [ - "client_secret_basic", - "client_secret_post" - ], - "claims_supported" => [ - "aud", - "auth_time", - "created_at", - "email", - "email_verified", - "exp", - "family_name", - "given_name", - "iat", - "identities", - "iss", - "name", - "nickname", - "phone_number", - "picture", - "sub" - ], - "request_uri_parameter_supported" => false, - "request_parameter_supported" => false - } - - Plug.Conn.resp(conn, 200, Jason.encode!(attrs)) - end) - - {bypass, "#{endpoint}/.well-known/openid-configuration"} - end - - def generate_openid_connect_token(provider, identity, claims \\ %{}) do - claims = - Map.merge( - %{ - "email" => identity.provider_identifier, - "sub" => identity.provider_identifier, - "aud" => provider.adapter_config["client_id"], - "exp" => DateTime.utc_now() |> DateTime.add(10, :second) |> DateTime.to_unix() - }, - claims - ) - - {sign_openid_connect_token(claims), claims} - end - - def sign_openid_connect_token(claims) do - jwk = jwks_attrs() - - {_alg, token} = - jwk - |> JOSE.JWK.from() - |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) - |> JOSE.JWS.compact() - - token - end - - defp fetch_conn_params(conn) do - opts = Plug.Parsers.init(parsers: [:urlencoded, :json], pass: ["*/*"], json_decoder: Jason) - - conn - |> Plug.Conn.fetch_query_params() - |> Plug.Parsers.call(opts) - end - - defp counter do - System.unique_integer([:positive]) - end -end diff --git a/elixir/apps/domain/test/support/fixtures/config_fixtures.ex b/elixir/apps/domain/test/support/fixtures/config.ex similarity index 73% rename from elixir/apps/domain/test/support/fixtures/config_fixtures.ex rename to elixir/apps/domain/test/support/fixtures/config.ex index 2181ea98c..ea9b64e34 100644 --- a/elixir/apps/domain/test/support/fixtures/config_fixtures.ex +++ b/elixir/apps/domain/test/support/fixtures/config.ex @@ -1,6 +1,6 @@ -defmodule Domain.ConfigFixtures do +defmodule Domain.Fixtures.Config do + use Domain.Fixture alias Domain.Config - alias Domain.AccountsFixtures def configuration_attrs(attrs \\ %{}) do Enum.into(attrs, %{ @@ -9,15 +9,13 @@ defmodule Domain.ConfigFixtures do end def upsert_configuration(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) + attrs = configuration_attrs(attrs) {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) end) - attrs = configuration_attrs(attrs) - {:ok, configuration} = Config.get_account_config_by_account_id(account.id) |> Config.update_config(attrs) diff --git a/elixir/apps/domain/test/support/fixtures/devices.ex b/elixir/apps/domain/test/support/fixtures/devices.ex new file mode 100644 index 000000000..f49c0e5d0 --- /dev/null +++ b/elixir/apps/domain/test/support/fixtures/devices.ex @@ -0,0 +1,66 @@ +defmodule Domain.Fixtures.Devices do + use Domain.Fixture + alias Domain.Devices + + def device_attrs(attrs \\ %{}) do + Enum.into(attrs, %{ + external_id: Ecto.UUID.generate(), + name: "device-#{unique_integer()}", + public_key: unique_public_key() + }) + end + + def create_device(attrs \\ %{}) do + attrs = device_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + if relation = attrs[:actor] || attrs[:identity] do + Repo.get!(Domain.Accounts.Account, relation.account_id) + else + Fixtures.Accounts.create_account(assoc_attrs) + end + end) + + {actor, attrs} = + pop_assoc_fixture(attrs, :actor, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{type: :account_admin_user, account: account}) + |> Fixtures.Actors.create_actor() + end) + + {identity, attrs} = + pop_assoc_fixture(attrs, :identity, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account, actor: actor}) + |> Fixtures.Auth.create_identity() + end) + + {subject, attrs} = + pop_assoc_fixture(attrs, :subject, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{ + account: account, + identity: identity, + actor: [type: :account_admin_user] + }) + |> Fixtures.Auth.create_subject() + end) + + {:ok, device} = Devices.upsert_device(attrs, subject) + %{device | online?: false} + end + + def delete_device(device) do + device = Repo.preload(device, :account) + + subject = + Fixtures.Auth.create_subject( + account: device.account, + actor: [type: :account_admin_user] + ) + + {:ok, device} = Devices.delete_device(device, subject) + device + end +end diff --git a/elixir/apps/domain/test/support/fixtures/devices_fixtures.ex b/elixir/apps/domain/test/support/fixtures/devices_fixtures.ex deleted file mode 100644 index 42af1fb55..000000000 --- a/elixir/apps/domain/test/support/fixtures/devices_fixtures.ex +++ /dev/null @@ -1,64 +0,0 @@ -defmodule Domain.DevicesFixtures do - alias Domain.Repo - alias Domain.Devices - alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures} - - def device_attrs(attrs \\ %{}) do - Enum.into(attrs, %{ - external_id: Ecto.UUID.generate(), - name: "device-#{counter()}", - public_key: public_key() - }) - end - - def create_device(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - if relation = attrs[:actor] || attrs[:identity] do - Repo.get!(Domain.Accounts.Account, relation.account_id) - else - AccountsFixtures.create_account() - end - end) - - {actor, attrs} = - Map.pop_lazy(attrs, :actor, fn -> - ActorsFixtures.create_actor(type: :account_admin_user, account: account) - end) - - {identity, attrs} = - Map.pop_lazy(attrs, :identity, fn -> - AuthFixtures.create_identity(account: account, actor: actor) - end) - - {subject, attrs} = - Map.pop_lazy(attrs, :subject, fn -> - AuthFixtures.create_subject(identity) - end) - - attrs = device_attrs(attrs) - - {:ok, device} = Devices.upsert_device(attrs, subject) - device - end - - def delete_device(device) do - device = Repo.preload(device, :account) - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: device.account) - identity = AuthFixtures.create_identity(account: device.account, actor: actor) - subject = AuthFixtures.create_subject(identity) - {:ok, device} = Devices.delete_device(device, subject) - device - end - - def public_key do - :crypto.strong_rand_bytes(32) - |> Base.encode64() - end - - defp counter do - System.unique_integer([:positive]) - end -end diff --git a/elixir/apps/domain/test/support/fixtures/gateways.ex b/elixir/apps/domain/test/support/fixtures/gateways.ex new file mode 100644 index 000000000..bd64999bb --- /dev/null +++ b/elixir/apps/domain/test/support/fixtures/gateways.ex @@ -0,0 +1,118 @@ +defmodule Domain.Fixtures.Gateways do + use Domain.Fixture + alias Domain.Gateways + + def group_attrs(attrs \\ %{}) do + Enum.into(attrs, %{ + name_prefix: "group-#{unique_integer()}", + tags: ["aws", "aws-us-east-#{unique_integer()}"], + tokens: [%{}] + }) + end + + def create_group(attrs \\ %{}) do + attrs = group_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {subject, attrs} = + pop_assoc_fixture(attrs, :subject, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account, actor: [type: :account_admin_user]}) + |> Fixtures.Auth.create_subject() + end) + + {:ok, group} = Gateways.create_group(attrs, subject) + group + end + + def delete_group(group) do + group = Repo.preload(group, :account) + + subject = + Fixtures.Auth.create_subject( + account: group.account, + actor: [type: :account_admin_user] + ) + + {:ok, group} = Gateways.delete_group(group, subject) + group + end + + def create_token(attrs \\ %{}) do + attrs = Enum.into(attrs, %{}) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {group, attrs} = + pop_assoc_fixture(attrs, :group, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account}) + |> create_group() + end) + + {subject, _attrs} = + pop_assoc_fixture(attrs, :subject, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account, actor: [type: :account_admin_user]}) + |> Fixtures.Auth.create_subject() + end) + + Gateways.Token.Changeset.create(account, subject) + |> Ecto.Changeset.put_change(:group_id, group.id) + |> Repo.insert!() + end + + def gateway_attrs(attrs \\ %{}) do + Enum.into(attrs, %{ + external_id: Ecto.UUID.generate(), + name_suffix: "gw-#{Domain.Crypto.rand_string(5)}", + public_key: unique_public_key(), + last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}} + }) + end + + def create_gateway(attrs \\ %{}) do + attrs = gateway_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {group, attrs} = + pop_assoc_fixture(attrs, :group, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account}) + |> create_group() + end) + + {token, attrs} = + Map.pop_lazy(attrs, :token, fn -> + hd(group.tokens) + end) + + {:ok, gateway} = Gateways.upsert_gateway(token, attrs) + %{gateway | online?: false} + end + + def delete_gateway(gateway) do + gateway = Repo.preload(gateway, :account) + + subject = + Fixtures.Auth.create_subject( + account: gateway.account, + actor: [type: :account_admin_user] + ) + + {:ok, gateway} = Gateways.delete_gateway(gateway, subject) + gateway + end +end diff --git a/elixir/apps/domain/test/support/fixtures/gateways_fixtures.ex b/elixir/apps/domain/test/support/fixtures/gateways_fixtures.ex deleted file mode 100644 index 68e0fa3e9..000000000 --- a/elixir/apps/domain/test/support/fixtures/gateways_fixtures.ex +++ /dev/null @@ -1,132 +0,0 @@ -defmodule Domain.GatewaysFixtures do - alias Domain.AccountsFixtures - alias Domain.Repo - alias Domain.Gateways - alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures} - - def group_attrs(attrs \\ %{}) do - Enum.into(attrs, %{ - name_prefix: "group-#{counter()}", - tags: ["aws", "aws-us-east-#{counter()}"], - tokens: [%{}] - }) - end - - def create_group(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {subject, attrs} = - Map.pop_lazy(attrs, :subject, fn -> - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - AuthFixtures.create_subject(identity) - end) - - attrs = group_attrs(attrs) - - {:ok, group} = Gateways.create_group(attrs, subject) - group - end - - def delete_group(group) do - group = Repo.preload(group, :account) - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: group.account) - identity = AuthFixtures.create_identity(account: group.account, actor: actor) - subject = AuthFixtures.create_subject(identity) - {:ok, group} = Gateways.delete_group(group, subject) - group - end - - def create_token(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {group, attrs} = - case Map.pop(attrs, :group, %{}) do - {%Gateways.Group{} = group, _attrs} -> - {group, attrs} - - {group_attrs, attrs} -> - group_attrs = Enum.into(group_attrs, %{account: account}) - {create_group(group_attrs), attrs} - end - - {subject, _attrs} = - Map.pop_lazy(attrs, :subject, fn -> - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - AuthFixtures.create_subject(identity) - end) - - Gateways.Token.Changeset.create_changeset(account, subject) - |> Ecto.Changeset.put_change(:group_id, group.id) - |> Repo.insert!() - end - - def gateway_attrs(attrs \\ %{}) do - Enum.into(attrs, %{ - external_id: Ecto.UUID.generate(), - name_suffix: "gw-#{Domain.Crypto.rand_string(5)}", - public_key: public_key(), - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}} - }) - end - - def create_gateway(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {group, attrs} = - case Map.pop(attrs, :group, %{}) do - {%Gateways.Group{} = group, attrs} -> - {group, attrs} - - {group_attrs, attrs} -> - group_attrs = Enum.into(group_attrs, %{account: account}) - group = create_group(group_attrs) - {group, attrs} - end - - {token, attrs} = - Map.pop_lazy(attrs, :token, fn -> - hd(group.tokens) - end) - - attrs = gateway_attrs(attrs) - - {:ok, gateway} = Gateways.upsert_gateway(token, attrs) - %{gateway | online?: false} - end - - def delete_gateway(gateway) do - gateway = Repo.preload(gateway, :account) - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: gateway.account) - identity = AuthFixtures.create_identity(account: gateway.account, actor: actor) - subject = AuthFixtures.create_subject(identity) - {:ok, gateway} = Gateways.delete_gateway(gateway, subject) - gateway - end - - def public_key do - :crypto.strong_rand_bytes(32) - |> Base.encode64() - end - - defp counter do - System.unique_integer([:positive]) - end -end diff --git a/elixir/apps/domain/test/support/fixtures/network_fixtures.ex b/elixir/apps/domain/test/support/fixtures/network.ex similarity index 76% rename from elixir/apps/domain/test/support/fixtures/network_fixtures.ex rename to elixir/apps/domain/test/support/fixtures/network.ex index 2537885ea..e63e50d04 100644 --- a/elixir/apps/domain/test/support/fixtures/network_fixtures.ex +++ b/elixir/apps/domain/test/support/fixtures/network.ex @@ -1,14 +1,13 @@ -defmodule Domain.NetworkFixtures do - alias Domain.Repo +defmodule Domain.Fixtures.Network do + use Domain.Fixture alias Domain.Network - alias Domain.AccountsFixtures def address_attrs(attrs \\ %{}) do attrs = Enum.into(attrs, %{account_id: nil, address: nil, type: nil}) {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) end) {:ok, inet} = Domain.Types.INET.cast(attrs.address) diff --git a/elixir/apps/domain/test/support/fixtures/notifications_fixtures.ex b/elixir/apps/domain/test/support/fixtures/notifications_fixtures.ex deleted file mode 100644 index f206df846..000000000 --- a/elixir/apps/domain/test/support/fixtures/notifications_fixtures.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Domain.NotificationsFixtures do - @moduledoc """ - This module defines test helpers for creating notifications. - """ - - @doc """ - Generate a notification. - """ - def notification_fixture(attrs \\ []) do - Enum.into(attrs, %{ - type: :error, - user: "test@localhost", - message: "Notification test text", - timestamp: DateTime.utc_now() - }) - end -end diff --git a/elixir/apps/domain/test/support/fixtures/policies.ex b/elixir/apps/domain/test/support/fixtures/policies.ex new file mode 100644 index 000000000..e24576985 --- /dev/null +++ b/elixir/apps/domain/test/support/fixtures/policies.ex @@ -0,0 +1,45 @@ +defmodule Domain.Fixtures.Policies do + use Domain.Fixture + + def policy_attrs(attrs \\ %{}) do + Enum.into(attrs, %{ + name: "policy-#{unique_integer()}" + }) + end + + def create_policy(attrs \\ %{}) do + attrs = policy_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {subject, attrs} = + pop_assoc_fixture(attrs, :subject, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account, actor: [type: :account_admin_user]}) + |> Fixtures.Auth.create_subject() + end) + + {actor_group_id, attrs} = + pop_assoc_fixture_id(attrs, :actor_group, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account, subject: subject}) + |> Fixtures.Actors.create_group() + end) + + {resource_id, attrs} = + pop_assoc_fixture_id(attrs, :resource, fn -> + Fixtures.Resources.create_resource(account: account, subject: subject) + end) + + {:ok, policy} = + attrs + |> Map.put(:actor_group_id, actor_group_id) + |> Map.put(:resource_id, resource_id) + |> Domain.Policies.create_policy(subject) + + policy + end +end diff --git a/elixir/apps/domain/test/support/fixtures/policies_fixtures.ex b/elixir/apps/domain/test/support/fixtures/policies_fixtures.ex deleted file mode 100644 index e64687e5a..000000000 --- a/elixir/apps/domain/test/support/fixtures/policies_fixtures.ex +++ /dev/null @@ -1,61 +0,0 @@ -defmodule Domain.PoliciesFixtures do - alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures, ResourcesFixtures} - - def policy_attrs(attrs \\ %{}) do - name = "policy-#{counter()}" - - Enum.into(attrs, %{ - name: name, - actor_group_id: nil, - resource_id: nil - }) - end - - def create_policy(attrs \\ %{}) do - attrs = policy_attrs(attrs) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {subject, attrs} = - Map.pop_lazy(attrs, :subject, fn -> - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - AuthFixtures.create_subject(identity) - end) - - {actor_group, attrs} = - Map.pop_lazy(attrs, :actor_group, fn -> - ActorsFixtures.create_group(account: account, subject: subject) - end) - - {actor_group_id, attrs} = - Map.pop_lazy(attrs, :actor_group, fn -> - actor_group.id - end) - - {resource, attrs} = - Map.pop_lazy(attrs, :resource, fn -> - ResourcesFixtures.create_resource(account: account, subject: subject) - end) - - {resource_id, attrs} = - Map.pop_lazy(attrs, :resource, fn -> - resource.id - end) - - {:ok, policy} = - attrs - |> Map.put(:actor_group_id, actor_group_id) - |> Map.put(:resource_id, resource_id) - |> Domain.Policies.create_policy(subject) - - policy - end - - defp counter do - System.unique_integer([:positive]) - end -end diff --git a/elixir/apps/domain/test/support/fixtures/relays.ex b/elixir/apps/domain/test/support/fixtures/relays.ex new file mode 100644 index 000000000..8d2214b2a --- /dev/null +++ b/elixir/apps/domain/test/support/fixtures/relays.ex @@ -0,0 +1,124 @@ +defmodule Domain.Fixtures.Relays do + use Domain.Fixture + alias Domain.Relays + + def group_attrs(attrs \\ %{}) do + Enum.into(attrs, %{ + name: "group-#{unique_integer()}", + tokens: [%{}] + }) + end + + def create_group(attrs \\ %{}) do + attrs = group_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {subject, attrs} = + pop_assoc_fixture(attrs, :subject, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account, actor: [type: :account_admin_user]}) + |> Fixtures.Auth.create_subject() + end) + + {:ok, group} = Relays.create_group(attrs, subject) + group + end + + def create_global_group(attrs \\ %{}) do + attrs = group_attrs(attrs) + {:ok, group} = Relays.create_global_group(attrs) + group + end + + def delete_group(group) do + group = Repo.preload(group, :account) + + subject = + Fixtures.Auth.create_subject( + account: group.account, + actor: [type: :account_admin_user] + ) + + {:ok, group} = Relays.delete_group(group, subject) + group + end + + def create_token(attrs \\ %{}) do + attrs = Enum.into(attrs, %{}) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {group, attrs} = + pop_assoc_fixture(attrs, :group, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account}) + |> create_group() + end) + + {subject, _attrs} = + pop_assoc_fixture(attrs, :subject, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account, actor: [type: :account_admin_user]}) + |> Fixtures.Auth.create_subject() + end) + + Relays.Token.Changeset.create(account, subject) + |> Ecto.Changeset.put_change(:group_id, group.id) + |> Repo.insert!() + end + + def relay_attrs(attrs \\ %{}) do + ipv4 = unique_ipv4() + + Enum.into(attrs, %{ + ipv4: ipv4, + ipv6: unique_ipv6(), + last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + last_seen_remote_ip: ipv4 + }) + end + + def create_relay(attrs \\ %{}) do + attrs = relay_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {group, attrs} = + pop_assoc_fixture(attrs, :group, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account}) + |> create_group() + end) + + {token, attrs} = + Map.pop_lazy(attrs, :token, fn -> + hd(group.tokens) + end) + + {:ok, relay} = Relays.upsert_relay(token, attrs) + %{relay | online?: false} + end + + def delete_relay(relay) do + relay = Repo.preload(relay, :account) + + subject = + Fixtures.Auth.create_subject( + account: relay.account, + actor: [type: :account_admin_user] + ) + + {:ok, relay} = Relays.delete_relay(relay, subject) + relay + end +end diff --git a/elixir/apps/domain/test/support/fixtures/relays_fixtures.ex b/elixir/apps/domain/test/support/fixtures/relays_fixtures.ex deleted file mode 100644 index 5987a07c6..000000000 --- a/elixir/apps/domain/test/support/fixtures/relays_fixtures.ex +++ /dev/null @@ -1,152 +0,0 @@ -defmodule Domain.RelaysFixtures do - alias Domain.Repo - alias Domain.Relays - alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures} - - def group_attrs(attrs \\ %{}) do - Enum.into(attrs, %{ - name: "group-#{counter()}", - tokens: [%{}] - }) - end - - def create_group(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {subject, attrs} = - Map.pop_lazy(attrs, :subject, fn -> - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - AuthFixtures.create_subject(identity) - end) - - attrs = group_attrs(attrs) - - {:ok, group} = Relays.create_group(attrs, subject) - group - end - - def create_global_group(attrs \\ %{}) do - attrs = group_attrs(attrs) - {:ok, group} = Relays.create_global_group(attrs) - group - end - - def delete_group(group) do - group = Repo.preload(group, :account) - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: group.account) - identity = AuthFixtures.create_identity(account: group.account, actor: actor) - subject = AuthFixtures.create_subject(identity) - {:ok, group} = Relays.delete_group(group, subject) - group - end - - def create_token(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {group, attrs} = - case Map.pop(attrs, :group, %{}) do - {%Relays.Group{} = group, attrs} -> - {group, attrs} - - {group_attrs, attrs} -> - group_attrs = Enum.into(group_attrs, %{account: account}) - {create_group(group_attrs), attrs} - end - - {subject, _attrs} = - Map.pop_lazy(attrs, :subject, fn -> - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - AuthFixtures.create_subject(identity) - end) - - Relays.Token.Changeset.create_changeset(account, subject) - |> Ecto.Changeset.put_change(:group_id, group.id) - |> Repo.insert!() - end - - def relay_attrs(attrs \\ %{}) do - ipv4 = random_ipv4() - - Enum.into(attrs, %{ - ipv4: ipv4, - ipv6: random_ipv6(), - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: ipv4 - }) - end - - def create_relay(attrs \\ %{}) do - attrs = Enum.into(attrs, %{}) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {group, attrs} = - case Map.pop(attrs, :group, %{}) do - {%Relays.Group{} = group, attrs} -> - {group, attrs} - - {group_attrs, attrs} -> - group_attrs = Enum.into(group_attrs, %{account: account}) - group = create_group(group_attrs) - {group, attrs} - end - - {token, attrs} = - Map.pop_lazy(attrs, :token, fn -> - hd(group.tokens) - end) - - attrs = relay_attrs(attrs) - - {:ok, relay} = Relays.upsert_relay(token, attrs) - %{relay | online?: false} - end - - def delete_relay(relay) do - relay = Repo.preload(relay, :account) - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: relay.account) - identity = AuthFixtures.create_identity(account: relay.account, actor: actor) - subject = AuthFixtures.create_subject(identity) - {:ok, relay} = Relays.delete_relay(relay, subject) - relay - end - - def public_key do - :crypto.strong_rand_bytes(32) - |> Base.encode64() - end - - defp counter do - System.unique_integer([:positive]) - end - - defp random_ipv4 do - number = counter() - <> = <> - {a, b, c, d} - end - - defp random_ipv6 do - number = counter() - - <> = <> - - {a, b, c, d, e, f, g, h} - end -end diff --git a/elixir/apps/domain/test/support/fixtures/resources.ex b/elixir/apps/domain/test/support/fixtures/resources.ex new file mode 100644 index 000000000..638d0bae7 --- /dev/null +++ b/elixir/apps/domain/test/support/fixtures/resources.ex @@ -0,0 +1,48 @@ +defmodule Domain.Fixtures.Resources do + use Domain.Fixture + + def resource_attrs(attrs \\ %{}) do + address = "admin-#{unique_integer()}.mycorp.com" + + Enum.into(attrs, %{ + address: address, + name: address, + type: :dns, + filters: [ + %{protocol: :tcp, ports: [80, 433]}, + %{protocol: :udp, ports: [100..200]} + ] + }) + end + + def create_resource(attrs \\ %{}) do + attrs = resource_attrs(attrs) + + {account, attrs} = + pop_assoc_fixture(attrs, :account, fn assoc_attrs -> + Fixtures.Accounts.create_account(assoc_attrs) + end) + + {connections, attrs} = + Map.pop_lazy(attrs, :connections, fn -> + Enum.map(1..2, fn _ -> + gateway = Fixtures.Gateways.create_gateway(account: account) + %{gateway_group_id: gateway.group_id} + end) + end) + + {subject, attrs} = + pop_assoc_fixture(attrs, :subject, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{account: account, actor: [type: :account_admin_user]}) + |> Fixtures.Auth.create_subject() + end) + + {:ok, resource} = + attrs + |> Map.put(:connections, connections) + |> Domain.Resources.create_resource(subject) + + resource + end +end diff --git a/elixir/apps/domain/test/support/fixtures/resources_fixtures.ex b/elixir/apps/domain/test/support/fixtures/resources_fixtures.ex deleted file mode 100644 index 5f2d598c0..000000000 --- a/elixir/apps/domain/test/support/fixtures/resources_fixtures.ex +++ /dev/null @@ -1,53 +0,0 @@ -# TODO: Domain.Fixtures.Resources -defmodule Domain.ResourcesFixtures do - alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures, GatewaysFixtures} - - def resource_attrs(attrs \\ %{}) do - address = "admin-#{counter()}.mycorp.com" - - Enum.into(attrs, %{ - address: address, - name: address, - type: :dns, - filters: [ - %{protocol: :tcp, ports: [80, 433]}, - %{protocol: :udp, ports: [100..200]} - ] - }) - end - - def create_resource(attrs \\ %{}) do - attrs = resource_attrs(attrs) - - {account, attrs} = - Map.pop_lazy(attrs, :account, fn -> - AccountsFixtures.create_account() - end) - - {connections, attrs} = - Map.pop_lazy(attrs, :gateway_groups, fn -> - Enum.map(1..2, fn _ -> - gateway = GatewaysFixtures.create_gateway(account: account) - %{gateway_group_id: gateway.group_id} - end) - end) - - {subject, attrs} = - Map.pop_lazy(attrs, :subject, fn -> - actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account) - identity = AuthFixtures.create_identity(account: account, actor: actor) - AuthFixtures.create_subject(identity) - end) - - {:ok, resource} = - attrs - |> Map.put(:connections, connections) - |> Domain.Resources.create_resource(subject) - - resource - end - - defp counter do - System.unique_integer([:positive]) - end -end diff --git a/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex b/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex index fe79393a5..878d5d5a9 100644 --- a/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex +++ b/elixir/apps/domain/test/support/mocks/google_workspace_directory.ex @@ -27,7 +27,7 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do %{"address" => "b@ext.firez.xxx"} ], "etag" => "\"ET-61Bnx4\"", - "id" => "ID4", + "id" => "USER_ID1", "includeInGlobalAddressList" => true, "ipWhitelisted" => false, "isAdmin" => false, @@ -75,7 +75,7 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do %{"address" => "f@ext.firez.xxx"} ], "etag" => "\"ET-c\"", - "id" => "ID104288977385815201534", + "id" => "USER_ID2", "includeInGlobalAddressList" => true, "ipWhitelisted" => false, "isAdmin" => false, @@ -117,11 +117,11 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do "creationTime" => "2022-05-31T19:17:41.000Z", "customerId" => "CustomerID1", "emails" => [ - %{"address" => "gabriel@firez.xxx", "primary" => true}, + %{"address" => "g@firez.xxx", "primary" => true}, %{"address" => "gabi@firez.xxx"} ], "etag" => "\"ET\"", - "id" => "ID2", + "id" => "USER_ID3", "includeInGlobalAddressList" => true, "ipWhitelisted" => false, "isAdmin" => false, @@ -137,14 +137,14 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do "fullName" => "Gabriel Steinberg", "givenName" => "Gabriel" }, - "nonEditableAliases" => ["gabriel@ext.firez.xxx"], + "nonEditableAliases" => ["g@ext.firez.xxx"], "orgUnitPath" => "/Engineering", - "primaryEmail" => "gabriel@firez.xxx", + "primaryEmail" => "g@firez.xxx", "suspended" => false }, %{ "agreedToTerms" => true, - "aliases" => ["jam@firez.xxx"], + "aliases" => ["j@firez.xxx"], "archived" => false, "changePasswordAtNextLogin" => false, "creationTime" => "2022-04-19T21:54:21.000Z", @@ -156,7 +156,7 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do %{"address" => "j@ext.firez.xxx"} ], "etag" => "\"ET-4Z0R5TBJvppLL8\"", - "id" => "ID1", + "id" => "USER_ID4", "includeInGlobalAddressList" => true, "ipWhitelisted" => false, "isAdmin" => true, @@ -215,9 +215,9 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do "description" => "Engineering team", "etag" => "\"ET\"", "blockInheritance" => false, - "orgUnitId" => "ID1", + "orgUnitId" => "OU_ID1", "orgUnitPath" => "/Engineering", - "parentOrgUnitId" => "ID0", + "parentOrgUnitId" => "OU_ID0", "parentOrgUnitPath" => "/" } ] @@ -248,7 +248,7 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do [ %{ "kind" => "admin#directory#group", - "id" => "ID1", + "id" => "GROUP_ID1", "etag" => "\"ET\"", "email" => "i@fiez.xxx", "name" => "Infrastructure", @@ -264,20 +264,20 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do }, %{ "kind" => "admin#directory#group", - "id" => "ID2", + "id" => "GROUP_ID2", "etag" => "\"ET\"", - "email" => "mktn@fiez.xxx", - "name" => "Marketing", + "email" => "eng@fiez.xxx", + "name" => "Engineering", "directMembersCount" => "1", - "description" => "Firezone Marketing team", + "description" => "Firezone Engineering team", "adminCreated" => true, "nonEditableAliases" => [ - "mktn@ext.fiez.xxx" + "eng@ext.fiez.xxx" ] }, %{ "kind" => "admin#directory#group", - "id" => "ID9c6y382yitz1j", + "id" => "GROUP_ID9c6y382yitz1j", "etag" => "\"ET\"", "email" => "sec@fiez.xxx", "name" => "Security", @@ -317,7 +317,7 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do %{ "kind" => "admin#directory#member", "etag" => "\"ET\"", - "id" => "115559319585605830228", + "id" => "USER_ID1", "email" => "b@firez.xxx", "role" => "MEMBER", "type" => "USER", @@ -326,7 +326,7 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do %{ "kind" => "admin#directory#member", "etag" => "\"ET\"", - "id" => "115559319585605830218", + "id" => "USER_ID4", "email" => "j@firez.xxx", "role" => "MEMBER", "type" => "USER", @@ -335,8 +335,8 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do %{ "kind" => "admin#directory#member", "etag" => "\"ET\"", - "id" => "115559319585605830518", - "email" => "f@firez.xxx", + "id" => "USER_ID3", + "email" => "g@firez.xxx", "role" => "MEMBER", "type" => "USER", "status" => "INACTIVE" @@ -344,7 +344,7 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do %{ "kind" => "admin#directory#member", "etag" => "\"ET\"", - "id" => "02xcytpi3twf80c", + "id" => "GROUP_ID1", "email" => "eng@firez.xxx", "role" => "MEMBER", "type" => "GROUP", @@ -353,7 +353,7 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do %{ "kind" => "admin#directory#member", "etag" => "\"ET\"", - "id" => "02xcytpi16r56td", + "id" => "GROUP_ID2", "email" => "sec@firez.xxx", "role" => "MEMBER", "type" => "GROUP", diff --git a/elixir/apps/domain/test/support/mocks/openid_connect.ex b/elixir/apps/domain/test/support/mocks/openid_connect.ex new file mode 100644 index 000000000..d1e19382f --- /dev/null +++ b/elixir/apps/domain/test/support/mocks/openid_connect.ex @@ -0,0 +1,208 @@ +defmodule Domain.Mocks.OpenIDConnect do + def discovery_document_server do + bypass = Bypass.open() + endpoint = "http://localhost:#{bypass.port}" + test_pid = self() + + Bypass.stub(bypass, "GET", "/.well-known/jwks.json", fn conn -> + attrs = %{"keys" => [jwks()]} + Plug.Conn.resp(conn, 200, Jason.encode!(attrs)) + end) + + Bypass.stub(bypass, "GET", "/.well-known/openid-configuration", fn conn -> + conn = fetch_conn_params(conn) + send(test_pid, {:request, conn}) + + attrs = %{ + "issuer" => "#{endpoint}/", + "authorization_endpoint" => "#{endpoint}/authorize", + "token_endpoint" => "#{endpoint}/oauth/token", + "device_authorization_endpoint" => "#{endpoint}/oauth/device/code", + "userinfo_endpoint" => "#{endpoint}/userinfo", + "mfa_challenge_endpoint" => "#{endpoint}/mfa/challenge", + "jwks_uri" => "#{endpoint}/.well-known/jwks.json", + "registration_endpoint" => "#{endpoint}/oidc/register", + "revocation_endpoint" => "#{endpoint}/oauth/revoke", + "end_session_endpoint" => "https://example.com", + "scopes_supported" => [ + "openid", + "profile", + "offline_access", + "name", + "given_name", + "family_name", + "nickname", + "email", + "email_verified", + "picture", + "created_at", + "identities", + "phone", + "address" + ], + "response_types_supported" => [ + "code", + "token", + "id_token", + "code token", + "code id_token", + "token id_token", + "code token id_token" + ], + "code_challenge_methods_supported" => [ + "S256", + "plain" + ], + "response_modes_supported" => [ + "query", + "fragment", + "form_post" + ], + "subject_types_supported" => [ + "public" + ], + "id_token_signing_alg_values_supported" => [ + "HS256", + "RS256" + ], + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "client_secret_post" + ], + "claims_supported" => [ + "aud", + "auth_time", + "created_at", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "identities", + "iss", + "name", + "nickname", + "phone_number", + "picture", + "sub" + ], + "request_uri_parameter_supported" => false, + "request_parameter_supported" => false + } + + Plug.Conn.resp(conn, 200, Jason.encode!(attrs)) + end) + + bypass + end + + def expect_refresh_token(bypass, attrs \\ %{}) do + test_pid = self() + + Bypass.expect(bypass, "POST", "/oauth/token", fn conn -> + conn = fetch_conn_params(conn) + send(test_pid, {:request, conn}) + Plug.Conn.resp(conn, 200, Jason.encode!(attrs)) + end) + + bypass + end + + def expect_refresh_token_failure(bypass, attrs \\ %{}) do + test_pid = self() + + Bypass.expect(bypass, "POST", "/oauth/token", fn conn -> + conn = fetch_conn_params(conn) + send(test_pid, {:request, conn}) + Plug.Conn.resp(conn, 401, Jason.encode!(attrs)) + end) + + bypass + end + + def expect_userinfo(bypass, attrs \\ %{}) do + test_pid = self() + + Bypass.expect(bypass, "GET", "/userinfo", fn conn -> + attrs = + Map.merge( + %{ + "sub" => "353690423699814251281", + "name" => "Ada Lovelace", + "given_name" => "Ada", + "family_name" => "Lovelace", + "picture" => + "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg", + "email" => "ada@example.com", + "email_verified" => true, + "locale" => "en" + }, + attrs + ) + + conn = fetch_conn_params(conn) + send(test_pid, {:request, conn}) + Plug.Conn.resp(conn, 200, Jason.encode!(attrs)) + end) + + bypass + end + + def generate_openid_connect_token(provider, identity, claims \\ %{}) do + claims = + Map.merge( + %{ + "email" => identity.provider_identifier, + "sub" => identity.provider_identifier, + "aud" => provider.adapter_config["client_id"], + "exp" => DateTime.utc_now() |> DateTime.add(10, :second) |> DateTime.to_unix() + }, + claims + ) + + {sign_openid_connect_token(claims), claims} + end + + def sign_openid_connect_token(claims) do + jwk = Domain.Mocks.OpenIDConnect.jwks() + + {_alg, token} = + jwk + |> JOSE.JWK.from() + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() + + token + end + + def jwks do + %{ + "alg" => "RS256", + "d" => + "X8TM24Zqbiha9geYYk_vZpANu16IadJLJLJ7ucTc3JaMbK8NCYNcHMoXKnNYPFxmq-UWAEIwh-2" <> + "txOiOxuChVrblpfyE4SBJio1T0AUcCwmm8U6G-CsSHMMzWTt2dMTnArHjdyAIgOVRW5SVzhTT" <> + "taf4JY-47S-fbcJ7g0hmBbVih5i1sE2fad4I4qFHT-YFU_pnUHbteR6GQuRW4r03Eon8Aje6a" <> + "l2AxcYnfF8_cSOIOpkDgGavTtGYhhZPi2jZ7kPm6QGkNW5CyfEq5PGB6JOihw-XIFiiMzYgx0" <> + "52rnzoqALoLheXrI0By4kgHSmcqOOmq7aiOff45rlSbpsR", + "e" => "AQAB", + "kid" => "example@firezone.dev", + "kty" => "RSA", + "n" => + "qlKll8no4lPYXNSuTTnacpFHiXwPOv_htCYvIXmiR7CWhiiOHQqj7KWXIW7TGxyoLVIyeRM4mwv" <> + "kLI-UgsSMYdEKTT0j7Ydjrr0zCunPu5Gxr2yOmcRaszAzGxJL5DwpA0V40RqMlm5OuwdqS4To" <> + "_p9LlLxzMF6RZe1OqslV5RZ4Y8FmrWq6BV98eIziEHL0IKdsAIrrOYkkcLDdQeMNuTp_yNB8X" <> + "l2TdWSdsbRomrs2dCtCqZcXTsy2EXDceHvYhgAB33nh_w17WLrZQwMM-7kJk36Kk54jZd7i80" <> + "AJf_s_plXn1mEh-L5IAL1vg3a9EOMFUl-lPiGqc3td_ykH", + "use" => "sig" + } + end + + defp fetch_conn_params(conn) do + opts = Plug.Parsers.init(parsers: [:urlencoded, :json], pass: ["*/*"], json_decoder: Jason) + + conn + |> Plug.Conn.fetch_query_params() + |> Plug.Parsers.call(opts) + end +end diff --git a/elixir/apps/web/.formatter.exs b/elixir/apps/web/.formatter.exs index a04ffb66e..fe4eec393 100644 --- a/elixir/apps/web/.formatter.exs +++ b/elixir/apps/web/.formatter.exs @@ -4,6 +4,7 @@ inputs: ["*.{xml.heex,html.heex,ex,exs}", "{config,lib,test}/**/*.{xml.heex,html.heex,ex,exs}"], locals_without_parens: [ assert_authenticated: 2, - assert_unauthenticated: 1 + assert_unauthenticated: 1, + assert_lists_equal: 2 ] ] diff --git a/elixir/apps/web/lib/web.ex b/elixir/apps/web/lib/web.ex index f17d519a8..d5c08adc0 100644 --- a/elixir/apps/web/lib/web.ex +++ b/elixir/apps/web/lib/web.ex @@ -136,9 +136,10 @@ defmodule Web do def components do quote do import Web.CoreComponents + import Web.NavigationComponents import Web.FormComponents import Web.TableComponents - import Web.NavigationComponents + import Web.PageComponents import Web.Gettext end end diff --git a/elixir/apps/web/lib/web/components/core_components.ex b/elixir/apps/web/lib/web/components/core_components.ex index b643256a2..c991988eb 100644 --- a/elixir/apps/web/lib/web/components/core_components.ex +++ b/elixir/apps/web/lib/web/components/core_components.ex @@ -292,6 +292,7 @@ defmodule Web.CoreComponents do attr :title, :string, default: nil attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup" attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container" + attr :style, :string, default: "pill" slot :inner_block, doc: "the optional inner block that renders the flash message" @@ -301,9 +302,10 @@ defmodule Web.CoreComponents do :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)} id={@id} class={[ - "p-4 mb-4 text-sm rounded-lg flash-#{@kind}", + "p-4 text-sm flash-#{@kind}", @kind == :info && "text-yellow-800 bg-yellow-50 dark:bg-gray-800 dark:text-yellow-300", - @kind == :error && "text-red-800 bg-red-50 dark:bg-gray-800 dark:text-red-400" + @kind == :error && "text-red-800 bg-red-50 dark:bg-gray-800 dark:text-red-400", + @style != "wide" && "mb-4 rounded-lg" ]} role="alert" {@rest} @@ -488,6 +490,10 @@ defmodule Web.CoreComponents do | + <:empty> + nothing + + <:item> home @@ -501,26 +507,86 @@ defmodule Web.CoreComponents do ``` - - Renders the following markup: - - home | profile | settings """ slot :separator, required: true, doc: "the slot for the separator" slot :item, required: true, doc: "the slots to intersperse with separators" + slot :empty, required: false, doc: "the slots to render when there are no items" def intersperse_blocks(assigns) do ~H""" - <%= for item <- Enum.intersperse(@item, :separator) do %> - <%= if item == :separator do %> - <%= render_slot(@separator) %> - <% else %> - <%= render_slot(item) %> + <%= if Enum.empty?(@item) do %> + <%= render_slot(@empty) %> + <% else %> + <%= for item <- Enum.intersperse(@item, :separator) do %> + <%= if item == :separator do %> + <%= render_slot(@separator) %> + <% else %> + <%= render_slot(item) %> + <% end %> <% end %> <% end %> """ end + @doc """ + Render children preview. + + Allows to render peeks into a schema preload by rendering a few of the children with a count of remaining ones. + + ## Examples + + ```heex + <.peek> + <:empty> + nobody + + + <:item :let={item}> + <%= item %> + + + <:separator> + , + + + <:tail :let={count}> + <%= count %> more. + + + ``` + """ + attr :peek, :any, + required: true, + doc: "a tuple with the total number of items and items for a preview" + + slot :empty, required: false, doc: "the slots to render when there are no items" + slot :item, required: true, doc: "the slots to intersperse with separators" + slot :separator, required: false, doc: "the slot for the separator" + slot :tail, required: true, doc: "the slots to render to show the remaining count" + + def peek(assigns) do + ~H""" +
+ <%= if Enum.empty?(@peek.items) do %> + <%= render_slot(@empty) %> + <% else %> + <% items = if @separator, do: Enum.intersperse(@peek.items, :separator), else: @peek.items %> + <%= for item <- items do %> + <%= if item == :separator do %> + <%= render_slot(@separator) %> + <% else %> + <%= render_slot(@item, item) %> + <% end %> + <% end %> + + length(@peek.items)} class="pl-1"> + <%= render_slot(@tail, @peek.count - length(@peek.items)) %> + + <% end %> +
+ """ + end + def status_page_widget(assigns) do ~H"""
@@ -547,7 +613,10 @@ defmodule Web.CoreComponents do assigns = assign(assigns, colors: colors) ~H""" - + <%= render_slot(@inner_block) %> """ @@ -571,16 +640,19 @@ defmodule Web.CoreComponents do Returns a string the represents a relative time for a given Datetime from the current time or a given base time """ - attr :datetime, DateTime, required: true + attr :datetime, DateTime, default: nil attr :relative_to, DateTime, required: false def relative_datetime(assigns) do assigns = assign_new(assigns, :relative_to, fn -> DateTime.utc_now() end) ~H""" - + <%= Cldr.DateTime.Relative.to_string!(@datetime, Web.CLDR, relative_to: @relative_to) %> + + never + """ end @@ -608,29 +680,78 @@ defmodule Web.CoreComponents do end @doc """ - Renders username + Renders creation timestamp and entity. """ - attr :schema, :map, required: true + attr :schema, :any, required: true - def owner(assigns) do - case assigns.schema.created_by do - :system -> - ~H""" - - System - - """ + def created_by(%{schema: %{created_by: :system}} = assigns) do + ~H""" + <.relative_datetime datetime={@schema.inserted_at} /> + """ + end - :identity -> - ~H""" - <.link - class="text-blue-600 hover:underline" - navigate={~p"/#{@schema.account_id}/actors/#{@schema.created_by_identity.actor.id}"} - > - <%= assigns.schema.created_by_identity.actor.name %> - - """ - end + def created_by(%{schema: %{created_by: :identity}} = assigns) do + ~H""" + <.relative_datetime datetime={@schema.inserted_at} /> by + <.link + class="text-blue-600 hover:underline" + navigate={~p"/#{@schema.account_id}/actors/#{@schema.created_by_identity.actor.id}"} + > + <%= assigns.schema.created_by_identity.actor.name %> + + """ + end + + def created_by(%{schema: %{created_by: :provider}} = assigns) do + ~H""" + synced <.relative_datetime datetime={@schema.inserted_at} /> from + <.link + class="text-blue-600 hover:underline" + navigate={Web.Settings.IdentityProviders.Components.view_provider(@schema.provider)} + > + <%= @schema.provider.name %> + + """ + end + + attr :identity, :string, required: true + + def identity_identifier(assigns) do + ~H""" + + <.link + navigate={Web.Settings.IdentityProviders.Components.view_provider(@identity.provider)} + data-provider-id={@identity.provider.id} + title={@identity.provider.adapter} + class={~w[ + text-xs font-medium + rounded-l + py-0.5 pl-2.5 pr-1.5 + text-blue-800 dark:text-blue-300 + bg-blue-100 dark:bg-blue-900]} + > + <%= @identity.provider.name %> + + + <%= @identity.provider_identifier %> + + + (deleted) + + + (provider disabled) + + + (provider deleted) + + + """ end @doc """ diff --git a/elixir/apps/web/lib/web/components/form_components.ex b/elixir/apps/web/lib/web/components/form_components.ex index b7198a340..66a6eb104 100644 --- a/elixir/apps/web/lib/web/components/form_components.ex +++ b/elixir/apps/web/lib/web/components/form_components.ex @@ -25,6 +25,10 @@ defmodule Web.FormComponents do attr :label, :string, default: nil attr :value, :any + attr :value_id, :any, + default: nil, + doc: "the function for generating the value from the list of schemas for select inputs" + attr :type, :string, default: "text", values: ~w(checkbox color date datetime-local email file hidden month number password @@ -50,7 +54,21 @@ defmodule Web.FormComponents do |> assign(field: nil, id: assigns.id || field.id) |> assign(:errors, Enum.map(field.errors, &translate_error(&1))) |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end) - |> assign_new(:value, fn -> field.value end) + |> assign_new(:value, fn -> + if assigns.value_id do + Enum.map(field.value, fn + %Ecto.Changeset{} = value -> + value + |> Ecto.Changeset.apply_changes() + |> assigns.value_id.() + + value -> + assigns.value_id.(value) + end) + else + field.value + end + end) |> input() end @@ -369,15 +387,20 @@ defmodule Web.FormComponents do """ attr :navigate, :any, required: true, doc: "Path to navigate to" + attr :class, :string, default: "" slot :inner_block, required: true def add_button(assigns) do ~H""" - <.link navigate={@navigate} class={~w[ - flex items-center justify-center text-white bg-primary-500 hover:bg-primary-600 - focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-4 py-2 - dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800 - ]}> + <.link + navigate={@navigate} + class={[ + "flex items-center justify-center text-white bg-primary-500 hover:bg-primary-600", + "focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-4 py-2", + "dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800", + @class + ]} + > <.icon name="hero-plus" class="h-3.5 w-3.5 mr-2" /> <%= render_slot(@inner_block) %> diff --git a/elixir/apps/web/lib/web/components/layouts/app.html.heex b/elixir/apps/web/lib/web/components/layouts/app.html.heex index afc50ee9c..bbb7a595d 100644 --- a/elixir/apps/web/lib/web/components/layouts/app.html.heex +++ b/elixir/apps/web/lib/web/components/layouts/app.html.heex @@ -1,100 +1,4 @@ - +<.topbar subject={@subject} /> <.sidebar> <.sidebar_item navigate={~p"/#{@account}/dashboard"} icon="hero-chart-bar-square-solid"> @@ -136,7 +40,6 @@ <:item navigate={~p"/#{@account}/settings/account"}>Account <:item navigate={~p"/#{@account}/settings/identity_providers"}>Identity Providers <:item navigate={~p"/#{@account}/settings/dns"}>DNS - <:item navigate={~p"/#{@account}/settings/api_tokens"}>API <:bottom> diff --git a/elixir/apps/web/lib/web/components/navigation_components.ex b/elixir/apps/web/lib/web/components/navigation_components.ex index 094bfb1ae..077039d6d 100644 --- a/elixir/apps/web/lib/web/components/navigation_components.ex +++ b/elixir/apps/web/lib/web/components/navigation_components.ex @@ -3,6 +3,88 @@ defmodule Web.NavigationComponents do use Web, :verified_routes import Web.CoreComponents + attr :subject, :any, required: true + + def topbar(assigns) do + ~H""" + + """ + end + + attr :subject, :any, required: true + + def subject_dropdown(assigns) do + ~H""" +
+ + <%= @subject.actor.name %> + + + <%= @subject.identity.provider_identifier %> + +
+ + + """ + end + slot :bottom, required: false slot :inner_block, @@ -29,6 +111,38 @@ defmodule Web.NavigationComponents do """ end + attr :id, :string, required: true, doc: "ID of the nav group container" + slot :button, required: true + slot :dropdown, required: true + + def dropdown(assigns) do + ~H""" + +
+ <%= render_slot(@dropdown) %> +
+ """ + end + attr :icon, :string, required: true attr :navigate, :string, required: true slot :inner_block, required: true @@ -107,7 +221,13 @@ defmodule Web.NavigationComponents do @doc """ Renders breadcrumbs section, for elements `<.breadcrumb />` component should be used. """ - attr :home_path, :string, required: true, doc: "The path for to the home page for a user." + attr :account, :any, + required: false, + default: nil, + doc: "Account assign which will be used to fetch the home path." + + # TODO: remove this attribute + attr :home_path, :string, required: false, doc: "The path for to the home page for a user." slot :inner_block, required: true, doc: "Breadcrumb entries" def breadcrumbs(assigns) do @@ -116,7 +236,7 @@ defmodule Web.NavigationComponents do
  1. <.link - navigate={@home_path} + navigate={if @account, do: ~p"/#{@account}/dashboard", else: @home_path} class="inline-flex items-center text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" > <.icon name="hero-home-solid" class="w-4 h-4 mr-2" /> Home diff --git a/elixir/apps/web/lib/web/components/page_components.ex b/elixir/apps/web/lib/web/components/page_components.ex new file mode 100644 index 000000000..3995885d0 --- /dev/null +++ b/elixir/apps/web/lib/web/components/page_components.ex @@ -0,0 +1,125 @@ +defmodule Web.PageComponents do + use Phoenix.Component + use Web, :verified_routes + import Web.CoreComponents + + slot :title, required: true, doc: "The title of the page to be displayed" + + slot :action, required: false, doc: "A slot for action to the right from title" do + attr :type, :string + attr :navigate, :string + attr :icon, :string + end + + slot :content, required: true, doc: "A slot for content which lists the entries" do + attr :flash, :any, doc: "The flash to be displayed above the content" + end + + slot :danger_zone, + required: false, + doc: "A slot for dangerous actions to be displayed below the content" + + def page(assigns) do + ~H""" + <.header> + <:title> + <%= render_slot(@title) %> + + + <:actions :if={not Enum.empty?(@action)}> + <.action_button + :for={action <- @action} + type={Map.get(action, :type)} + navigate={action.navigate} + icon={action.icon} + > + <%= render_slot(action) %> + + + + +
    +
    +
    + <.flash kind={:info} flash={Map.get(content, :flash)} style="wide" /> + <.flash kind={:error} flash={Map.get(content, :flash)} style="wide" /> +
    + + <%= render_slot(content) %> +
    + +
    + <.header> + <:title> + Danger zone + + <:actions> + <%= render_slot(@danger_zone) %> + + +
    +
    + """ + end + + attr :navigate, :string, + required: false, + doc: """ + The path to navigate to, when set an tag will be used, + otherwise a + """ + end + + defp button_style do + [ + "flex items-center justify-center", + "px-4 py-2 rounded-lg", + "font-medium text-sm", + "focus:ring-4 focus:outline-none", + "phx-submit-loading:opacity-75" + ] + end + + defp button_style("danger") do + button_style() ++ + [ + "text-red-600", + "border border-red-600", + "hover:text-white hover:bg-red-600 focus:ring-red-300", + "dark:border-red-500 dark:text-red-500 dark:hover:text-white dark:hover:bg-red-600 dark:focus:ring-red-900" + ] + end + + defp button_style(_style) do + button_style() ++ + [ + "text-white", + "bg-primary-500", + "hover:bg-primary-600", + "focus:ring-primary-300", + "dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800" + ] + end +end diff --git a/elixir/apps/web/lib/web/components/table_components.ex b/elixir/apps/web/lib/web/components/table_components.ex index e2d489ff1..43792ad66 100644 --- a/elixir/apps/web/lib/web/components/table_components.ex +++ b/elixir/apps/web/lib/web/components/table_components.ex @@ -14,7 +14,7 @@ defmodule Web.TableComponents do ~H""" - + <%= col[:label] %> <.icon :if={col[:sortable] == "true"} @@ -54,18 +54,33 @@ defmodule Web.TableComponents do > <%= render_slot(col, @mapper.(@row)) %> - - -