Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions lib/mailgun_logger/roles/roles.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,35 @@ defmodule MailgunLogger.Roles do
@superuser_role "superuser"
@admin_role "admin"

# Create an new role
@member_role "member"

# Task 1:
# Permissions for member is looking at the events + details on \events pages && Profile changes on /Profile
# (Not: stats, Accounts, Users) interface & router off limits
# Update seeds to add the member role in database to use in the application

# Task 2:
# Admin and superuser roles need to have the permissions to edit user & create user
# can add 1 or multiple roles to a user
# An user cannot downgrade himself!
# Write an unit test (Phoenix/ExUnit) for these task and permissions

#########################################################

@default_actions ~w()
@default_actions ~w(view_profile)

@member_actions ~w(view_event view_stats view_graphs) ++ @default_actions

@admin_actions ~w(do_stuff) ++ @default_actions
@admin_actions ~w(
trigger_run view_users edit_user create_user
delete_user manage_accounts manage_admins
manage_members grant_admin_role grant_member_role
) ++ @member_actions

@superuser_actions ~w() ++ @admin_actions
@superuser_actions ~w(
manage_superusers grant_superuser_role
) ++ @admin_actions

#########################################################

Expand Down Expand Up @@ -55,6 +77,14 @@ defmodule MailgunLogger.Roles do
Enum.any?(roles, &can?(&1.name, action))
end

# Actions

# Add compile time permission
for action <- @member_actions do
action = String.to_atom(action)
def can?(@member_role, unquote(action)), do: true
end

for action <- @admin_actions do
action = String.to_atom(action)
def can?(@admin_role, unquote(action)), do: true
Expand All @@ -67,16 +97,37 @@ defmodule MailgunLogger.Roles do

def can?(_, _), do: false

# Include member in the helper functions
def is?(%User{roles: roles}, :superuser), do: is(roles, "superuser")
def is?(%User{roles: roles}, :admin), do: is(roles, "admin")
def is?(%User{roles: roles}, :member), do: is(roles, "member")
def is?(_, _), do: raise("Roles.is/2 requires roles to be preloaded")

defp is(roles, role) when is_binary(role), do: Enum.map(roles, & &1.name) |> Enum.member?(role)

def abilities(%User{roles: []}), do: []
def abilities(%User{roles: roles}), do: hd(roles) |> abilities()

def abilities(%Role{name: "member"}), do: @member_actions
def abilities(%Role{name: "admin"}), do: @admin_actions
def abilities(%Role{name: "superuser"}), do: @superuser_actions

def roles(%User{roles: roles}), do: Enum.map(roles, & &1.name)

# Guards - hidden cameras permission levels what you can do with a given role
def can_manage?(actor, target) do
required_permission =
cond do
is?(target, :superuser) -> :manage_superusers
is?(target, :admin) -> :manage_admins
true -> :manage_members
end

can?(actor, required_permission)
end

def can_grant_role?(actor, role_name) do
required_permission = :"grant_#{role_name}_role"
can?(actor, required_permission)
end
end
3 changes: 2 additions & 1 deletion lib/mailgun_logger/seeder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ defmodule MailgunLogger.Seeder do

# alias MailgunLogger.UserRole

@roles [%Role{name: "superuser"}, %Role{name: "admin"}]
# add member role so that you can seed the application with member role!
@roles [%Role{name: "superuser"}, %Role{name: "admin"}, %Role{name: "member"}]

def run do
Enum.each(@roles, &insert_if_new(&1))
Expand Down
19 changes: 17 additions & 2 deletions lib/mailgun_logger/users/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule MailgunLogger.User do
field(:reset_token, :string, default: nil)
field(:theme, :string, default: "system")
field(:password, :string, virtual: true)
field(:role_ids, {:array, :integer}, virtual: true, default: [])

many_to_many(:roles, Role, join_through: UserRole, on_replace: :delete)

Expand All @@ -45,24 +46,26 @@ defmodule MailgunLogger.User do
@spec changeset(User.t(), map()) :: Ecto.Changeset.t()
def changeset(%User{} = user, attrs \\ %{}) do
user
|> cast(attrs, [:firstname, :lastname, :email, :password, :theme])
|> cast(attrs, [:firstname, :lastname, :email, :password, :theme, :role_ids])
|> validate_required([:email, :password])
|> update_change(:email, &String.downcase/1)
|> validate_format(:email, @email_format)
|> validate_length(:password, min: 8)
|> unique_constraint(:email)
|> hash_password()
|> generate_token()
|> put_roles_if_present(attrs)
end

@doc false
@spec update_changeset(User.t(), map()) :: Ecto.Changeset.t()
def update_changeset(%User{} = user, attrs \\ %{}) do
user
|> cast(attrs, [:firstname, :lastname, :email, :theme])
|> cast(attrs, [:firstname, :lastname, :email, :theme, :role_ids])
|> update_change(:email, &String.downcase/1)
|> validate_format(:email, @email_format)
|> unique_constraint(:email)
|> put_roles_if_present(attrs)
end

@doc "Used when creating an admin, e.g. from the setup flow"
Expand Down Expand Up @@ -109,6 +112,18 @@ defmodule MailgunLogger.User do
put_change(changeset, :token, token)
end

# Replace the user's roles when role_ids is present in the params.
defp put_roles_if_present(changeset, %{"role_ids" => ids}) when is_list(ids) do
roles =
ids
|> Enum.reject(&(&1 in [nil, ""]))
|> Roles.get_roles_by_id()

Ecto.Changeset.put_assoc(changeset, :roles, roles)
end

defp put_roles_if_present(changeset, _), do: changeset

@doc false
def full_name(nil), do: ""
def full_name(%User{lastname: nil, firstname: nil, email: nil}), do: ""
Expand Down
12 changes: 11 additions & 1 deletion lib/mailgun_logger/users/users.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@ defmodule MailgunLogger.Users do
alias MailgunLogger.Repo
alias MailgunLogger.User


@type ecto_user() :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@type maybe_user() :: User.t() | nil

# Get count of the superusers to keep check that there is at least one superuser
@spec count_superusers() :: non_neg_integer()
def count_superusers do
from(u in User,
join: r in assoc(u, :roles),
where: r.name == "superuser",
select: count(u.id)
)
|> Repo.one()
end

@spec list_users() :: [User.t()]
def list_users() do
Repo.all(User) |> Repo.preload(:roles)
Expand Down
4 changes: 4 additions & 0 deletions lib/mailgun_logger_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ defmodule MailgunLoggerWeb do

def static_paths(), do: ~w(assets css fonts images js favicon.ico robots.txt)

# Import the plug in controllers so that plug: :authorize_action can be used
def controller do
quote do
use Phoenix.Controller, formats: [html: "View", json: "View"]
import Plug.Conn
use Gettext, backend: MailgunLoggerWeb.Gettext
alias MailgunLoggerWeb.Router.Helpers, as: Routes

# import the plug in every controller as Plug
alias MailgunLoggerWeb.Plugs.Authorize

unquote(verified_routes())
end
end
Expand Down
13 changes: 13 additions & 0 deletions lib/mailgun_logger_web/controllers/account_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ defmodule MailgunLoggerWeb.AccountController do
alias MailgunLogger.Accounts
alias MailgunLogger.Account

plug Authorize

@action_permissions %{
index: :manage_accounts,
new: :manage_accounts,
create: :manage_accounts,
edit: :manage_accounts,
update: :manage_accounts,
delete: :manage_accounts
}

def action_permissions, do: @action_permissions

def index(conn, _) do
accounts = Accounts.list_accounts()
render(conn, :index, accounts: accounts)
Expand Down
11 changes: 11 additions & 0 deletions lib/mailgun_logger_web/controllers/event_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ defmodule MailgunLoggerWeb.EventController do
alias MailgunLogger.Events
alias MailgunLogger.Accounts

# Set permissions for this controller
plug Authorize

@action_permissions %{
index: :view_event,
show: :view_event,
stored_message: :view_event
}

def action_permissions, do: @action_permissions

def index(conn, params) do
accounts = Accounts.list_accounts()

Expand Down
13 changes: 13 additions & 0 deletions lib/mailgun_logger_web/controllers/page_controller.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
defmodule MailgunLoggerWeb.PageController do
use MailgunLoggerWeb, :controller

plug Authorize

@action_permissions %{
index: :view_event,
stats: :view_stats,
graphs: :view_graphs,
# admin only
trigger_run: :trigger_run,
non_affiliation: :view_event
}

def action_permissions, do: @action_permissions

alias MailgunLogger.Events
alias MailgunLogger.Accounts

Expand Down
9 changes: 9 additions & 0 deletions lib/mailgun_logger_web/controllers/profile_controller.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
defmodule MailgunLoggerWeb.ProfileController do
use MailgunLoggerWeb, :controller

plug Authorize

@action_permissions %{
edit: :view_profile,
update: :view_profile
}

def action_permissions, do: @action_permissions

alias MailgunLogger.Users
alias MailgunLogger.User

Expand Down
Loading