lib/glific/access_control/flow_role.ex

defmodule Glific.AccessControl.FlowRole do
  @moduledoc """
  A pipe for managing the role flows
  """
  use Ecto.Schema
  import Ecto.Changeset
  import Ecto.Query, warn: false

  alias __MODULE__

  alias Glific.{
    AccessControl.Role,
    AccessControl.UserRole,
    Flows.Flow,
    Partners.Organization,
    Repo,
    Users.User
  }

  use Ecto.Schema
  import Ecto.Changeset

  @required_fields [:role_id, :flow_id, :organization_id]

  @type t() :: %__MODULE__{
          __meta__: Ecto.Schema.Metadata.t(),
          id: non_neg_integer | nil,
          role: Role.t() | Ecto.Association.NotLoaded.t() | nil,
          flow: Flow.t() | Ecto.Association.NotLoaded.t() | nil,
          organization_id: non_neg_integer | nil,
          organization: Organization.t() | Ecto.Association.NotLoaded.t() | nil
        }

  schema "flow_roles" do
    belongs_to :role, Role
    belongs_to :flow, Flow
    belongs_to :organization, Organization
  end

  @doc """
  Standard changeset pattern we use for all data types
  """
  @spec changeset(FlowRole.t(), map()) :: Ecto.Changeset.t()
  def changeset(role_flow, attrs) do
    role_flow
    |> cast(attrs, @required_fields)
    |> validate_required(@required_fields)
    |> unique_constraint(@required_fields)
    |> foreign_key_constraint(:role_id)
    |> foreign_key_constraint(:flow_id)
  end

  @doc """
  Creates a access control.
  ## Examples
      iex> create_flow_role(%{field: value})
      {:ok, %FlowRole{}}
      iex> create_flow_role(%{field: bad_value})
      {:error, %Ecto.Changeset{}}
  """
  @spec create_flow_role(map()) :: {:ok, FlowRole.t()} | {:error, Ecto.Changeset.t()}
  def create_flow_role(attrs \\ %{}) do
    %FlowRole{}
    |> changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Update flow roles based on add_role_ids and delete_role_ids and return number_deleted as integer and roles added as access_controls
  """
  @spec update_flow_roles(map()) :: map()
  def update_flow_roles(
        %{
          flow_id: flow_id,
          add_role_ids: add_role_ids
        } = attrs
      ) do
    access_controls =
      Enum.reduce(
        add_role_ids,
        [],
        fn role_id, acc ->
          case create_flow_role(Map.merge(attrs, %{role_id: role_id, flow_id: flow_id})) do
            {:ok, access_control} -> [access_control | acc]
            _ -> acc
          end
        end
      )

    {number_deleted, _} =
      if Map.has_key?(attrs, :delete_role_ids),
        do: delete_flow_roles_by_role_ids(flow_id, attrs.delete_role_ids),
        else: {0, attrs}

    %{
      number_deleted: number_deleted,
      access_controls: access_controls
    }
  end

  @doc """
  Delete flow roles
  """
  @spec delete_flow_roles_by_role_ids(integer, list()) :: {integer(), nil | [term()]}
  def delete_flow_roles_by_role_ids(flow_id, role_ids) do
    fields = {{:flow_id, flow_id}, {:role_id, role_ids}}
    Repo.delete_relationships_by_ids(FlowRole, fields)
  end

  @doc """
  Filtering entity object based on user role
  """
  @spec check_access(Ecto.Query.t(), User.t()) :: Ecto.Query.t()
  def check_access(entity_list, user) do
    sub_query =
      FlowRole
      |> select([rf], rf.flow_id)
      |> join(:inner, [rf], ru in UserRole, as: :ru, on: ru.role_id == rf.role_id)
      |> where([rf, ru: ru], ru.user_id == ^user.id)

    entity_list
    |> where(
      [f],
      f.id in subquery(sub_query)
    )
  end
end