Skip to content

Commit

Permalink
Merge pull request #27 from epochtalk/permissions
Browse files Browse the repository at this point in the history
Permissions
  • Loading branch information
unenglishable authored Jan 26, 2023
2 parents 5453ebf + 528d90c commit b9d2479
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 16 deletions.
2 changes: 2 additions & 0 deletions lib/epochtalk_server/auth/guardian.ex
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ defmodule EpochtalkServer.Auth.Guardian do
@doc """
Fetches the resource that is represented by claims.
For JWT this would normally be found in the `sub` field.
TODO(boka): refactor fetching of user session (L 161-202) into session.ex
"""
def resource_from_claims(%{"sub" => sub, "jti" => jti} = _claims) do
# Here we'll look up our resource from the claims, the subject can be
Expand Down
2 changes: 2 additions & 0 deletions lib/epochtalk_server_web/controllers/mention_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule EpochtalkServerWeb.MentionController do
alias EpochtalkServer.Models.Mention
alias EpochtalkServerWeb.ErrorHelpers
alias EpochtalkServerWeb.Helpers.Validate
alias EpochtalkServerWeb.Helpers.ACL

@doc """
Used to page `Mention` models for a specific `User`
Expand All @@ -16,6 +17,7 @@ defmodule EpochtalkServerWeb.MentionController do
with page <- Validate.cast(attrs, "page", :integer, min: 1),
limit <- Validate.cast(attrs, "limit", :integer, min: 1),
extended <- Validate.cast(attrs, "extended", :boolean),
:ok <- ACL.allow!(conn, "mentions.page"),
{:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
{:ok, mentions, data} <-
Mention.page_by_user_id(user.id, page, per_page: limit, extended: extended),
Expand Down
45 changes: 32 additions & 13 deletions lib/epochtalk_server_web/controllers/notification_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,50 @@ defmodule EpochtalkServerWeb.NotificationController do
alias EpochtalkServer.Models.Notification
alias EpochtalkServerWeb.ErrorHelpers
alias EpochtalkServerWeb.Helpers.Validate
alias EpochtalkServerWeb.Helpers.ACL

@doc """
Used to retrieve `Notification` counts for a specific `User`
"""
def counts(conn, attrs) do
with max <- Validate.cast(attrs, "max", :integer, min: 1),
{:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
do:
render(conn, "counts.json",
data: Notification.counts_by_user_id(user.id, max: max || 99)
),
else:
({:auth, nil} ->
ErrorHelpers.render_json_error(
conn,
400,
"Not logged in, cannot fetch notification counts"
))
with {:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
:ok <- ACL.allow!(conn, "notifications.counts"),
# {:ok, :allow} <- Authorization.grant(conn, counts_auth),
max <- Validate.cast(attrs, "max", :integer, min: 1) do
render(conn, "counts.json", data: Notification.counts_by_user_id(user.id, max: max || 99))
else
{:auth, nil} ->
ErrorHelpers.render_json_error(
conn,
400,
"Not logged in, cannot fetch notification counts"
)

{:access, false} ->
ErrorHelpers.render_json_error(
conn,
400,
"Not logged in, cannot fetch notification counts"
)
end
end

# defp counts_auth() do
# one = Task.one("some error one")
# two = Task.two("some error two")

# case Run.tasks(one, two) do
# :ok -> {:ok, :allow}
# {:error, msg} -> throw(msg)
# end
# end

@doc """
Used to dismiss `Notification` counts for a specific `User`
"""
def dismiss(conn, %{"id" => id}) do
with {:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
:ok <- ACL.allow!(conn, "notifications.dismiss"),
{_count, nil} <- Notification.dismiss(id) do
EpochtalkServerWeb.Endpoint.broadcast("user:#{user.id}", "refreshMentions", %{})
render(conn, "dismiss.json", data: %{success: true})
Expand Down
19 changes: 19 additions & 0 deletions lib/epochtalk_server_web/errors/custom_errors.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
defmodule EpochtalkServerWeb.CustomErrors do
# ACL Permissions
# credo:disable-for-next-line
defmodule InvalidPermission do
@moduledoc """
Exception raised when api request payload is incorrect
"""
@default_message "Forbidden, invalid permissions to perform this action"
defexception plug_status: 403, message: @default_message

@impl true
def exception(value) do
case value do
[message: nil] -> %InvalidPermission{}
[message: message] -> %InvalidPermission{message: message}
_ -> %InvalidPermission{}
end
end
end

# API Payload Handling
defmodule InvalidPayload do
@moduledoc """
Expand Down
91 changes: 91 additions & 0 deletions lib/epochtalk_server_web/helpers/acl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
defmodule EpochtalkServerWeb.Helpers.ACL do
alias EpochtalkServer.Models.Role
alias EpochtalkServer.Models.User
alias EpochtalkServerWeb.CustomErrors.InvalidPermission

@moduledoc """
Helper for checking authenticated user's permissions.
"""

@doc """
Checks if authenticated `User` is allowed to perform a specific action.
Compares a mask all of user's roles into a masked permission set and checks
if that permission set contains the specified `permission_path`.
Raises `CustomErrors.InvalidPermission` exception if the `User` does not have
the proper permissions.
"""
@spec allow!(Plug.Conn.t() | User.t(), permission_path :: String.t()) :: no_return | :ok
def allow!(%Plug.Conn{} = conn, permission_path) when is_binary(permission_path),
do: allow!(conn, permission_path, nil)

def allow!(%User{} = user, permission_path) when is_binary(permission_path),
do: allow!(%Plug.Conn{private: %{guardian_default_resource: user}}, permission_path, nil)

@doc """
Same as `ACL.allow!/2` but allows a custom error message to be raised if the
`User` does not have the proper permissions.
"""
@spec allow!(
Plug.Conn.t() | User.t() | any(),
permission_path :: String.t(),
error_msg :: String.t() | nil
) :: no_return | :ok
def allow!(
%Plug.Conn{private: %{guardian_default_resource: user}} = _conn,
permission_path,
error_msg
)
when is_binary(permission_path) do
# check if login is required to view forum
config = Application.get_env(:epochtalk_server, :frontend_config)
login_required = config[:login_required]
# default to user's roles > anonymous > private
user_roles =
if user == nil,
do:
if(login_required, do: [Role.by_lookup("private")], else: [Role.by_lookup("anonymous")]),
else: user.roles

authed_permissions = Role.get_masked_permissions(user_roles)

# convert path to array
path_list = String.split(permission_path, ".")
# append "allow" to array
path_list =
if List.last(path_list) != "allow",
do: path_list ++ ["allow"],
else: path_list

has_permission = Kernel.get_in(authed_permissions, path_list)
if !has_permission, do: raise(InvalidPermission, message: error_msg)
:ok
end

def allow!(
%User{} = user,
permission_path,
error_msg
)
when (is_binary(permission_path) and is_binary(error_msg)) or is_nil(error_msg),
do:
allow!(
%Plug.Conn{private: %{guardian_default_resource: user}},
permission_path,
error_msg
)

def allow!(
_,
permission_path,
error_msg
)
when (is_binary(permission_path) and is_binary(error_msg)) or is_nil(error_msg),
do:
allow!(
%Plug.Conn{private: %{guardian_default_resource: nil}},
permission_path,
error_msg
)
end
10 changes: 8 additions & 2 deletions lib/epochtalk_server_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,22 @@ defmodule EpochtalkServerWeb.Router do
end

pipeline :enforce_auth do
plug Guardian.Plug.Pipeline,
module: EpochtalkServer.Auth.Guardian,
error_handler: EpochtalkServerWeb.GuardianErrorHandler

plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"}
plug Guardian.Plug.LoadResource, allow_blank: false
plug Guardian.Plug.EnsureAuthenticated
end

scope "/api", EpochtalkServerWeb do
pipe_through [:api, :maybe_auth, :enforce_auth]
get "/authenticate", UserController, :authenticate
pipe_through [:api, :enforce_auth]
get "/users/preferences", PreferenceController, :preferences
get "/mentions", MentionController, :page
get "/notifications/counts", NotificationController, :counts
post "/notifications/dismiss", NotificationController, :dismiss
get "/authenticate", UserController, :authenticate
end

scope "/api", EpochtalkServerWeb do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ defmodule EpochtalkServerWeb.UserControllerTest do
test "errors with 401 when user is not logged in but trying to authenticate", %{conn: conn} do
conn = get(conn, Routes.user_path(conn, :authenticate))

assert %{"error" => "Unauthorized", "message" => "Unauthenticated"} =
assert %{"error" => "Unauthorized", "message" => "No resource found", "status" => 401} =
json_response(conn, 401)
end
end
Expand Down
Loading

0 comments on commit b9d2479

Please sign in to comment.