Commit b6e0ba82 authored by Mikko Ahlroth's avatar Mikko Ahlroth

Add permission check to GraphQL API and move it under browser section

Also refactored a couple of functions to the correct contexts
parent bc817088
Pipeline #16400515 passed with stage
in 1 minute and 38 seconds
defmodule CodeStatsWeb.PermissionUtils do
defmodule CodeStats.Profile.PermissionUtils do
@moduledoc """
Utility functions related to permissions in the system.
Utility functions related to profile permissions.
"""
alias CodeStats.User
......
defmodule CodeStats.Profile.PublicSchema do
use Absinthe.Schema
import Ecto.Query, only: [from: 2]
@generic_error "User not found or no access to profile."
import_types(CodeStats.Profile.SchemaObjects)
......@@ -10,24 +11,39 @@ defmodule CodeStats.Profile.PublicSchema do
@desc "Username of profile"
arg(:username, type: :string)
resolve(fn %{username: username}, _ ->
# TODO: Check if profile is private and user is allowed access
get_user_data(username)
resolve(fn
# User has authenticated and so is available in the context
%{username: username}, %{context: %{user: user}} ->
if username == user.username do
resolve_if_permission(user, user)
else
resolve_by_username(username, user)
end
# No authentication so check if can access profile
%{username: username}, _ ->
resolve_by_username(username, nil)
end)
end
end
defp get_user_data(username) do
query =
from(
u in CodeStats.User,
where: fragment("lower(?)", ^username) == fragment("lower(?)", u.username),
select: %{id: u.id, username: u.username, cache: u.cache, registered: u.inserted_at}
)
case CodeStats.Repo.one(query) do
nil -> {:error, "User not found"}
user -> {:ok, user}
# Resolve user by username, with authed user if exists
defp resolve_by_username(username, authed_user) do
case CodeStats.User.get_by_username(username) do
%CodeStats.User{} = user ->
resolve_if_permission(user, authed_user)
_ ->
{:error, @generic_error}
end
end
# Resolve user data if the given authed user has permission to view their data
defp resolve_if_permission(user, authed_user) do
if CodeStats.Profile.PermissionUtils.can_access_profile?(authed_user, user) do
{:ok, %{id: user.id, cache: user.cache, registered: user.inserted_at}}
else
{:error, @generic_error}
end
end
end
......@@ -65,6 +65,30 @@ defmodule CodeStats.User do
|> update_change(:password, &hash_password/1)
end
@doc """
Get user with the given username.
If second argument is true, case insensitive search is used instead.
Returns nil if user was not found.
"""
@spec get_by_username(String.t(), boolean) :: %__MODULE__{} | nil
def get_by_username(username, case_insensitive \\ false) do
query =
case case_insensitive do
false ->
from(u in __MODULE__, where: u.username == ^username)
true ->
from(
u in __MODULE__,
where: fragment("lower(?)", ^username) == fragment("lower(?)", u.username)
)
end
Repo.one(query)
end
@doc """
Calculate and store cached XP values for user.
......
......@@ -2,7 +2,6 @@ defmodule CodeStatsWeb.LiveUpdateSocket do
use Phoenix.Socket
alias CodeStats.User
alias CodeStatsWeb.AuthUtils
# Maximum age of token that is accepted. We want people to be able to leave the site open and be able to reconnect
# for a reasonable time but not use their token forever
......@@ -57,7 +56,7 @@ defmodule CodeStatsWeb.LiveUpdateSocket do
# Check that given token is valid, return user or nil if invalid
defp check_token(socket, token) do
with {:ok, data} <- Phoenix.Token.verify(socket, "user", token, max_age: @token_max_age),
%User{} = user <- AuthUtils.get_user(data) do
%User{} = user <- User.get_by_username(data) do
user
else
_ -> nil
......
......@@ -3,7 +3,6 @@ defmodule CodeStatsWeb.ProfileChannel do
alias CodeStats.User
alias CodeStats.User.Pulse
alias CodeStatsWeb.AuthUtils
alias CodeStatsWeb.ProfileUtils
@moduledoc """
......@@ -16,7 +15,7 @@ defmodule CodeStatsWeb.ProfileChannel do
# The profile is public, OR
# the current user is the same as the profile user.
with %User{} = user <- AuthUtils.get_user(username),
with %User{} = user <- User.get_by_username(username),
true <- !user.private_profile or socket.assigns[:user_id] === user.id,
updated_cache <- User.update_cached_xps(user),
preloaded_cache <- ProfileUtils.preload_cache_data(updated_cache, user),
......
......@@ -25,7 +25,7 @@ defmodule CodeStatsWeb.AuthController do
end
def login(conn, %{"username" => username, "password" => password} = params) do
with %User{} = user <- AuthUtils.get_user(username, true),
with %User{} = user <- User.get_by_username(username, true),
%Plug.Conn{} = conn <- AuthUtils.auth_user(conn, user, password),
%Plug.Conn{} = conn <- maybe_remember_me(conn, user, params) do
redirect(conn, to: profile_path(conn, :my_profile))
......
......@@ -3,7 +3,7 @@ defmodule CodeStatsWeb.ProfileController do
alias CodeStats.User
alias CodeStatsWeb.AuthUtils
alias CodeStatsWeb.PermissionUtils
alias CodeStats.Profile.PermissionUtils
alias CodeStatsWeb.ProfileUtils
def my_profile(conn, _params) do
......@@ -164,7 +164,7 @@ defmodule CodeStatsWeb.ProfileController do
defp get_user(username) do
with username <- fix_url_username(username),
%User{} = user <- AuthUtils.get_user(username, true) do
%User{} = user <- User.get_by_username(username, true) do
{:ok, user}
else
_ -> :error
......
......@@ -4,6 +4,8 @@ defmodule CodeStatsWeb.SetSessionUserPlug do
is_authed? should be used to check if user data is available before using the data set by
this plug.
This plug also adds the user status for use in Absinthe.
"""
import Plug.Conn
......@@ -21,8 +23,11 @@ defmodule CodeStatsWeb.SetSessionUserPlug do
if AuthUtils.is_authed?(conn) do
id = AuthUtils.get_current_user_id(conn)
query = from(u in User, where: u.id == ^id)
user = Repo.one(query)
put_private(conn, AuthUtils.private_info_key(), Repo.one(query))
conn
|> put_private(AuthUtils.private_info_key(), user)
|> put_private(:absinthe, %{context: %{user: user}})
else
put_private(conn, AuthUtils.private_info_key(), nil)
end
......
......@@ -5,7 +5,6 @@ defmodule CodeStatsWeb.Router do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_flash)
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
plug(CodeStatsWeb.RememberMePlug)
plug(CodeStatsWeb.SetSessionUserPlug)
......@@ -19,6 +18,10 @@ defmodule CodeStatsWeb.Router do
plug(CodeStatsWeb.AuthNotAllowedPlug)
end
pipeline :browser_changes_state do
plug(:protect_from_forgery)
end
pipeline :api do
plug(:accepts, ["json"])
end
......@@ -27,39 +30,42 @@ defmodule CodeStatsWeb.Router do
plug(CodeStatsWeb.MachineAuthRequiredPlug)
end
scope "/", CodeStatsWeb do
# Use the default browser stack
scope "/" do
pipe_through(:browser)
get("/", PageController, :index)
get("/", CodeStatsWeb.PageController, :index)
get("/api-docs", PageController, :api_docs)
get("/tos", PageController, :terms)
get("/plugins", PageController, :plugins)
get("/changes", PageController, :changes)
get("/api-docs", CodeStatsWeb.PageController, :api_docs)
get("/tos", CodeStatsWeb.PageController, :terms)
get("/plugins", CodeStatsWeb.PageController, :plugins)
get("/changes", CodeStatsWeb.PageController, :changes)
get("/aliases", AliasController, :list)
get("/aliases", CodeStatsWeb.AliasController, :list)
get("/battle", BattleController, :battle)
get("/battle", CodeStatsWeb.BattleController, :battle)
scope "/" do
pipe_through(:browser_changes_state)
pipe_through(:browser_unauth)
get("/login", AuthController, :render_login)
post("/login", AuthController, :login)
get("/signup", AuthController, :render_signup)
post("/signup", AuthController, :signup)
get("/forgot-password", AuthController, :render_forgot)
post("/forgot-password", AuthController, :forgot)
get("/reset-password/:token", AuthController, :render_reset)
put("/reset-password/:token", AuthController, :reset)
get("/login", CodeStatsWeb.AuthController, :render_login)
post("/login", CodeStatsWeb.AuthController, :login)
get("/signup", CodeStatsWeb.AuthController, :render_signup)
post("/signup", CodeStatsWeb.AuthController, :signup)
get("/forgot-password", CodeStatsWeb.AuthController, :render_forgot)
post("/forgot-password", CodeStatsWeb.AuthController, :forgot)
get("/reset-password/:token", CodeStatsWeb.AuthController, :render_reset)
put("/reset-password/:token", CodeStatsWeb.AuthController, :reset)
end
get("/logout", AuthController, :logout)
get("/logout", CodeStatsWeb.AuthController, :logout)
get("/users/:username", ProfileController, :profile)
get("/users/:username", CodeStatsWeb.ProfileController, :profile)
forward("/profile-graph", Absinthe.Plug, schema: CodeStats.Profile.PublicSchema)
forward("/profile-graphiql", Absinthe.Plug.GraphiQL, schema: CodeStats.Profile.PublicSchema)
scope "/my" do
scope "/my", CodeStatsWeb do
pipe_through(:browser_changes_state)
pipe_through(:browser_auth)
get("/profile", ProfileController, :my_profile)
......@@ -86,9 +92,6 @@ defmodule CodeStatsWeb.Router do
get("/users/:username", CodeStatsWeb.ProfileController, :profile_api)
forward("/users-graph", Absinthe.Plug, schema: CodeStats.Profile.PublicSchema)
forward("/users-graphiql", Absinthe.Plug.GraphiQL, schema: CodeStats.Profile.PublicSchema)
scope "/my" do
pipe_through(:api_machine_auth)
......
......@@ -54,30 +54,6 @@ defmodule CodeStatsWeb.AuthUtils do
@private_info_key
end
@doc """
Get user with the given username.
If second argument is true, case insensitive search is used instead.
Returns nil if user was not found.
"""
@spec get_user(String.t(), boolean) :: %User{} | nil
def get_user(username, case_insensitive \\ false) do
query =
case case_insensitive do
false ->
from(u in User, where: u.username == ^username)
true ->
from(
u in User,
where: fragment("lower(?)", ^username) == fragment("lower(?)", u.username)
)
end
Repo.one(query)
end
@doc """
Authenticate the given user in the given connection.
......@@ -182,7 +158,7 @@ defmodule CodeStatsWeb.AuthUtils do
@spec auth_machine(%Conn{}, String.t()) :: %Conn{}
def auth_machine(%Conn{} = conn, machine_token) do
with {username, machine_id} <- split_token(machine_token),
%User{} = user <- get_user(username),
%User{} = user <- User.get_by_username(username),
%Machine{} = machine <- get_machine(machine_id, user),
{:ok, _} <-
MessageVerifier.verify(machine_token, conn.secret_key_base <> machine.api_salt) do
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment