Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Board controller authorizations tests #83

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
91eca10
refactor(session): ? for boolean function is_active_for_user_id
unenglishable Sep 20, 2023
974859a
refactor(session): return errors on update() function
unenglishable Sep 20, 2023
cca252a
feat(session): implement has_sessions
unenglishable Sep 20, 2023
930f3b5
test(seed/users): seed no_login user
unenglishable Sep 20, 2023
8c331f7
test(support/conn_case): export no_login_user for testing
unenglishable Sep 20, 2023
7b2b0e8
test(session): test session update failure cases
unenglishable Sep 20, 2023
dbb913e
test(models/profile): implement fail case test for create
unenglishable Sep 22, 2023
414f8ad
test(support/data_case): add users/attrs to data case for testing
unenglishable Sep 22, 2023
a99a66f
test(models/profile): implement fail case test for existing user_id
unenglishable Sep 22, 2023
e93741c
test(models/profile): test upsert for avatar update
unenglishable Sep 22, 2023
7686fdc
test(models/profile): check profile default fields
unenglishable Sep 22, 2023
38e069e
test(models/profile): test upsert for all string fields
unenglishable Sep 22, 2023
0df42a5
test(session): check avatar gets updated in session after profile update
unenglishable Sep 22, 2023
7c9ac01
test(session): check that user role gets updated to admin
unenglishable Sep 22, 2023
9ff5c0c
test(session): use role cache to check that session roles were updated
unenglishable Sep 22, 2023
011aa93
test(session): check that user role can be added to superadmin
unenglishable Sep 22, 2023
a339bf6
test(session): fix super admin test, delete admin role, check user ro…
unenglishable Sep 22, 2023
6442d8e
test(session): check update ban_info after banning user
unenglishable Sep 28, 2023
096b8d5
test(session): update ban_info after unbanning user
unenglishable Sep 28, 2023
4620660
test(session): update session in conn_case after banning
unenglishable Sep 28, 2023
2d13f2d
test(session): update descriptions
unenglishable Sep 28, 2023
e898798
fix(session): use catch-all for setting ttl
unenglishable Oct 10, 2023
de30a45
refactor(session): log a warning in the event of session TTL catch-all
unenglishable Oct 10, 2023
bde1918
test(config): configure logger output with module and parameters
unenglishable Oct 10, 2023
dab43c7
docs(contributions): add note for using Logger
unenglishable Oct 10, 2023
f47eca1
style(): mix format
unenglishable Oct 13, 2023
13c5988
style(session): mix format
unenglishable Oct 13, 2023
d239b00
feat(session): populate session ban info if it exists
unenglishable Oct 26, 2023
fd7d3a2
fix(session): refer to user_key for ban_key base TTL
unenglishable Oct 26, 2023
5845b33
fix(session): refer to user_key for moderating_key base TTL
unenglishable Oct 26, 2023
0448df2
test(session): remove unused conn's
unenglishable Oct 26, 2023
e7fcfce
test(session): set attribute for max date
unenglishable Oct 26, 2023
f7ad8eb
test(session): check for ban_expiration on update
unenglishable Oct 26, 2023
5bcfdf5
test(session): check ban is expired on update
unenglishable Oct 26, 2023
0a600a3
feat(session): populate malicious score if it exists
unenglishable Oct 26, 2023
3f466b9
test(session): check malicious score on update
unenglishable Oct 26, 2023
fa165e7
test(session): check malicious score value > 1, + documentation
unenglishable Oct 27, 2023
32d67fd
test(factory/banned_address): use upsert
unenglishable Oct 27, 2023
7b3d380
refactor(models/banned_address): extract magic numbers from decay_for…
unenglishable Oct 27, 2023
3cce8b2
refactor(banned_address): extract magic numbers from calculate_malici…
unenglishable Oct 27, 2023
5696b94
test(session+user): update expected malicious scores
unenglishable Oct 27, 2023
c810e85
feat(errors/custom_errors): allow custom message for invalid payload …
akinsey Oct 30, 2023
4ebf2a5
feat(helpers/validate): add function to check mutual exclusivity of b…
akinsey Oct 31, 2023
fc5e828
refactor(controller/post): update comment todos
akinsey Oct 31, 2023
e0ebdfd
test(helper/validate): add doctest for mutually_exclusive! function
akinsey Oct 31, 2023
4b4f48a
docs(board_moderator): fix spelling
unenglishable Oct 31, 2023
d08d1a6
test(session): separate role/baninfo test blocks
unenglishable Oct 31, 2023
51d37d1
test(session): check moderating updates
unenglishable Oct 31, 2023
8a13d99
style(banned_address): mix format
unenglishable Oct 31, 2023
922dbc9
refactor(banned_address): move ip weight to variable set
unenglishable Oct 31, 2023
e750f25
test(session): mix format
unenglishable Oct 31, 2023
7e71ca6
docs(helpers/validate): fix spelling errors in docs
akinsey Oct 31, 2023
da3ed39
test(helpers/validate): move docs to test (for testing later)
unenglishable Oct 31, 2023
6ed9ede
test(helpers/validate): test mutually exclusive with several cases
unenglishable Oct 31, 2023
3913b9a
fix(helpers/validate): correctly implement mutually_exclusive
unenglishable Oct 31, 2023
b75855b
style(helpers/validate): mix format
unenglishable Oct 31, 2023
02ec3ed
test(helpers/validate): mix format
unenglishable Oct 31, 2023
b9a5d45
Merge pull request #80 from epochtalk/validate-mutually-exclusive-fixes
akinsey Oct 31, 2023
dae8789
Merge pull request #78 from epochtalk/validate-mutually-exclusive
unenglishable Oct 31, 2023
0f71c54
Merge remote-tracking branch 'origin/session-test' into board-control…
unenglishable Nov 1, 2023
54ee774
test(seed+conn_case): seed private user and prepare in conn_case
unenglishable Nov 2, 2023
f2364d5
test(controllers/board): check by_category errors on invalid permissions
unenglishable Nov 2, 2023
12f4d80
test(controllers/board): check invalid board read access for unauthen…
unenglishable Nov 2, 2023
241e510
test(controllers/board): update unauthenticated test description
unenglishable Nov 2, 2023
46e87d0
test(controllers/board): check board find when authenticated
unenglishable Nov 2, 2023
95ed14e
test(controllers/board): check board find when authenticated as admin
unenglishable Nov 2, 2023
55a1e6c
test(controllers/board): test super admin board find
unenglishable Nov 2, 2023
5afe7f5
Merge remote-tracking branch 'origin/main' into session-test
unenglishable Nov 3, 2023
3791a09
refactor(session): explain redis TTL special values, -1, -2
unenglishable Nov 3, 2023
55c7cc0
Merge pull request #82 from epochtalk/session-test
akinsey Nov 3, 2023
17ba087
Merge remote-tracking branch 'origin/main' into board-controller-auth…
unenglishable Nov 3, 2023
4d85649
Merge remote-tracking branch 'origin/board-controller-authorizations'…
unenglishable Nov 6, 2023
57bc64d
test(session): use new banned role in test
unenglishable Nov 6, 2023
5855fd9
test(controllers/board): mix format
unenglishable Nov 6, 2023
fe38cd6
test(support/conn_case): fix merge
unenglishable Nov 6, 2023
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
2 changes: 1 addition & 1 deletion CONTRIBUTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ conventions. Please refer to these guidelines!
* Write [code](https://en.wikipedia.org/wiki/Computer_programming)
in [Elixir](https://elixir-lang.org/) to implement your feature

(Instructions for your computer to do things)
* Use Logger instead of IO.puts/inspect

* Commit code using [Angular Commit Message](https://gist.github.com/brianclements/841ea7bffdb01346392c)
conventions.
Expand Down
6 changes: 5 additions & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ config :epochtalk_server, EpochtalkServerWeb.Endpoint,
config :epochtalk_server, EpochtalkServer.Mailer, adapter: Swoosh.Adapters.Test

# Print only warnings and errors during test
config :logger, level: :warning
# Accept module and parameters
config :logger, :console,
level: :warning,
format: "[$level] $message $metadata\n",
metadata: [:module, :parameters]

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime
26 changes: 17 additions & 9 deletions lib/epochtalk_server/models/banned_address.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ defmodule EpochtalkServer.Models.BannedAddress do
alias EpochtalkServer.Repo
alias EpochtalkServer.Models.BannedAddress

@one_week_in_ms 1000 * 60 * 60 * 24 * 7
@inital_amount 0.8897
@rate_of_decay 0.9644
@ip32_weight 1
@ip24_weight 0.04
@ip16_weight 0.0016

@moduledoc """
`BannedAddress` model, for performing actions relating to banning by ip/hostname
"""
Expand Down Expand Up @@ -222,11 +229,13 @@ defmodule EpochtalkServer.Models.BannedAddress do
0
end

ip32_score = calculate_ip32_score(ip)
ip24_score = calculate_ip24_score(ip)
ip16_score = calculate_ip16_score(ip)
# calculate ip scores with weight constants
ip32_score = calculate_ip32_score(ip) * @ip32_weight
ip24_score = calculate_ip24_score(ip) * @ip24_weight
ip16_score = calculate_ip16_score(ip) * @ip16_weight
# calculate malicious score using all scores
malicious_score = hostname_score + ip32_score + 0.04 + ip24_score + 0.0016 + ip16_score
malicious_score = hostname_score + ip32_score + ip24_score + ip16_score

if malicious_score < 1, do: nil, else: malicious_score

# invalid ip address, return nil for malicious score
Expand Down Expand Up @@ -310,11 +319,10 @@ defmodule EpochtalkServer.Models.BannedAddress do
# in ms since the weight was last updated
defp decay_for_time(time, weight) do
weight = Decimal.to_float(weight)
one_week = 1000 * 60 * 60 * 24 * 7
weeks = time / one_week
a = 0.8897
r = 0.9644
a ** ((r ** weeks - 1) / (r - 1)) * weight ** (r ** weeks)
weeks = time / @one_week_in_ms

@inital_amount ** ((@rate_of_decay ** weeks - 1) / (@rate_of_decay - 1)) *
weight ** (@rate_of_decay ** weeks)
end

# returns the decayed score given a banned address
Expand Down
6 changes: 3 additions & 3 deletions lib/epochtalk_server/models/board_moderator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ defmodule EpochtalkServer.Models.BoardModerator do
do: Repo.all(from(b in BoardModerator, select: b.board_id, where: b.user_id == ^user_id))

@doc """
Check if a specific `User` is moderater of a `Board` using a `Board` ID
Check if a specific `User` is moderator of a `Board` using a `Board` ID
"""
@spec user_is_moderator(
board_id :: non_neg_integer,
Expand All @@ -77,7 +77,7 @@ defmodule EpochtalkServer.Models.BoardModerator do
end

@doc """
Check if a specific `User` is moderater of a `Board` using a `Thread` ID
Check if a specific `User` is moderator of a `Board` using a `Thread` ID
"""
@spec user_is_moderator_with_thread_id(thread_id :: non_neg_integer, user_id :: non_neg_integer) ::
boolean
Expand All @@ -95,7 +95,7 @@ defmodule EpochtalkServer.Models.BoardModerator do
end

@doc """
Check if a specific `User` is moderater of a `Board` using a `Post` ID
Check if a specific `User` is moderator of a `Board` using a `Post` ID
"""
@spec user_is_moderator_with_post_id(
post_id :: non_neg_integer,
Expand Down
129 changes: 87 additions & 42 deletions lib/epochtalk_server/session.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
defmodule EpochtalkServer.Session do
@one_day_in_seconds 1 * 24 * 60 * 60
@four_weeks_in_seconds 4 * 7 * @one_day_in_seconds
# in redis, if a key exists and has no expiration, its TTL value is -1
# if the key does not exist, its TTL value is -2
@redis_ttl_no_expire_with_key -1

@moduledoc """
Manages `User` sessions in Redis. Used by Auth related `User` actions.
"""
require Logger
alias EpochtalkServer.Auth.Guardian
alias EpochtalkServer.Models.User
alias EpochtalkServer.Models.Role
Expand Down Expand Up @@ -52,30 +56,38 @@ defmodule EpochtalkServer.Session do
@doc """
Update session performs the following actions for active session:
* Saves `User` session info to redis (avatar, roles, moderating, ban info, etc)
* returns {:ok, user}
* returns {:ok, user} on success
* returns {:error, reason} on failure

DOES NOT change:
* User's session id, timestamp, ttl
* Guardian token
"""
@spec update(user_id :: non_neg_integer) :: {:ok, user :: User.t()}
@spec update(user_id :: non_neg_integer) ::
{:ok, user :: User.t()}
| {:error, reason :: String.t() | Postgrex.Error.t()}
def update(user_id) do
{:ok, user} = User.by_id(user_id)
avatar = if is_nil(user.profile), do: nil, else: user.profile.avatar
update_user_info(user.id, user.username, avatar: avatar)
update_roles(user.id, user.roles)

ban_info =
if is_nil(user.ban_info), do: %{}, else: %{ban_expiration: user.ban_info.expiration}

ban_info =
if !is_nil(user.malicious_score) && user.malicious_score >= 1,
do: Map.put(ban_info, :malicious_score, user.malicious_score),
else: ban_info

update_ban_info(user.id, ban_info)
update_moderating(user.id, user.moderating)
{:ok, user}
with {:ok, user} <- User.by_id(user_id),
true <- has_sessions?(user_id) do
avatar = if is_nil(user.profile), do: nil, else: user.profile.avatar
update_user_info(user.id, user.username, avatar: avatar)
update_roles(user.id, user.roles)

ban_info =
if is_nil(user.ban_info), do: %{}, else: %{ban_expiration: user.ban_info.expiration}

ban_info =
if !is_nil(user.malicious_score) && user.malicious_score >= 1,
do: Map.put(ban_info, :malicious_score, user.malicious_score),
else: ban_info

update_ban_info(user.id, ban_info)
update_moderating(user.id, user.moderating)
{:ok, user}
else
{:error, error} -> {:error, error}
false -> {:error, :no_sessions}
end
end

@doc """
Expand All @@ -87,7 +99,7 @@ defmodule EpochtalkServer.Session do
| {:error, reason :: String.t() | Redix.Error.t() | Redix.ConnectionError.t()}
def get_resource(user_id, session_id) do
# check if session is active in redis
is_active_for_user_id(session_id, user_id)
is_active_for_user_id?(session_id, user_id)
|> case do
{:error, error} ->
{:error, "Error finding resource from claims #{inspect(error)}"}
Expand Down Expand Up @@ -122,17 +134,19 @@ defmodule EpochtalkServer.Session do
else: resource

ban_key = generate_key(user_id, "baninfo")
ban_expiration = Redix.command!(:redix, ["HEXISTS", ban_key, "ban_expiration"])
ban_expiration_exists = Redix.command!(:redix, ["HEXISTS", ban_key, "ban_expiration"])
ban_expiration = Redix.command!(:redix, ["HGET", ban_key, "ban_expiration"])

resource =
if ban_expiration != 0,
if ban_expiration_exists != 0,
do: Map.put(resource, :ban_expiration, ban_expiration),
else: resource

malicious_score = Redix.command!(:redix, ["HEXISTS", ban_key, "malicious_score"])
malicious_score_exists = Redix.command!(:redix, ["HEXISTS", ban_key, "malicious_score"])
malicious_score = Redix.command!(:redix, ["HGET", ban_key, "malicious_score"])

resource =
if malicious_score != 0,
if malicious_score_exists != 0,
do: Map.put(resource, :malicious_score, malicious_score),
else: resource

Expand All @@ -153,7 +167,19 @@ defmodule EpochtalkServer.Session do
Redix.command(:redix, ["ZRANGE", session_key, 0, -1])
end

defp is_active_for_user_id(session_id, user_id) do
defp has_sessions?(user_id) do
session_key = generate_key(user_id, "sessions")
# check ZCARD for key
# returns number of sorted set elements at key
# returns 0 if key does not exist (set is empty)
case Redix.command(:redix, ["ZCARD", session_key]) do
{:error, error} -> {:error, error}
{:ok, 0} -> false
{:ok, _score} -> true
end
end

defp is_active_for_user_id?(session_id, user_id) do
session_key = generate_key(user_id, "sessions")
# check ZSCORE for user_id:session_id pair
# returns score if it is a member of the set
Expand Down Expand Up @@ -247,8 +273,10 @@ defmodule EpochtalkServer.Session do
moderating = moderating |> Enum.map(& &1.board_id)
# save/replace moderating boards to redis under "user:{user_id}:moderating"
moderating_key = generate_key(user_id, "moderating")
# get current TTL
{:ok, old_ttl} = Redix.command(:redix, ["TTL", moderating_key])
# use user_key for base ttl, since moderating may not be populated (TTL == -2)
user_key = generate_key(user_id, "user")
# get current TTL of session
{:ok, session_ttl} = Redix.command(:redix, ["TTL", user_key])
Redix.command(:redix, ["DEL", moderating_key])

unless moderating == [],
Expand All @@ -257,10 +285,10 @@ defmodule EpochtalkServer.Session do
# if ttl is provided
if ttl do
# maybe extend ttl
maybe_extend_ttl(moderating_key, ttl, old_ttl)
maybe_extend_ttl(moderating_key, ttl, session_ttl)
else
# otherwise, re-set old_ttl
maybe_extend_ttl(moderating_key, old_ttl)
# otherwise, re-set session_ttl from user_key
maybe_extend_ttl(moderating_key, session_ttl)
end
end

Expand Down Expand Up @@ -292,23 +320,26 @@ defmodule EpochtalkServer.Session do
defp update_ban_info(user_id, ban_info, ttl \\ nil) do
# save/replace ban_expiration to redis under "user:{user_id}:baninfo"
ban_key = generate_key(user_id, "baninfo")
# get current TTL
{:ok, old_ttl} = Redix.command(:redix, ["TTL", ban_key])
# use user_key for base ttl, since baninfo may not be populated (TTL == -2)
user_key = generate_key(user_id, "user")
# get current TTL of session
{:ok, session_ttl} = Redix.command(:redix, ["TTL", user_key])
Redix.command(:redix, ["HDEL", ban_key, "ban_expiration", "malicious_score"])

if ban_exp = Map.get(ban_info, :ban_expiration),
do: Redix.command(:redix, ["HSET", ban_key, "ban_expiration", ban_exp])
if ban_exp = Map.get(ban_info, :ban_expiration) do
Redix.command(:redix, ["HSET", ban_key, "ban_expiration", ban_exp])
end

if malicious_score = Map.get(ban_info, :malicious_score),
do: Redix.command(:redix, ["HSET", ban_key, "malicious_score", malicious_score])

# if ttl is provided
if ttl do
# maybe extend ttl
maybe_extend_ttl(ban_key, ttl, old_ttl)
maybe_extend_ttl(ban_key, ttl, session_ttl)
else
# otherwise, re-set old_ttl
maybe_extend_ttl(ban_key, old_ttl)
# otherwise, re-set session_ttl from user_key
maybe_extend_ttl(ban_key, session_ttl)
end
end

Expand Down Expand Up @@ -353,13 +384,27 @@ defmodule EpochtalkServer.Session do
maybe_extend_ttl(key, ttl, ttl)
end

# set ttl to max of old_ttl, new_ttl, and existing ttl
# - or @four_weeks_in_seconds if old and new ttl's are invalid
defp maybe_extend_ttl(key, new_ttl, old_ttl) do
if old_ttl > -1 do
# re-set old expiry only if old expiry was valid and key has no expiry
Redix.command(:redix, ["EXPIRE", key, old_ttl, "NX"])
else
# if old expiry was invalid, set new expiry only if key has no expiry
Redix.command(:redix, ["EXPIRE", key, new_ttl, "NX"])
cond do
old_ttl > @redis_ttl_no_expire_with_key ->
# re-set old expiry only if old expiry was valid and key has no expiry
Redix.command(:redix, ["EXPIRE", key, old_ttl, "NX"])

new_ttl > @redis_ttl_no_expire_with_key ->
# if old expiry was invalid, set new expiry only if new expiry is valid and key has no expiry
Redix.command(:redix, ["EXPIRE", key, new_ttl, "NX"])

true ->
# catch-all, in case both given expiries are invalid
Logger.warning("Invalid TTL's, setting max expiry", %{
module: __MODULE__,
parameters: "[old_ttl: #{old_ttl}, new_ttl: #{new_ttl}, key: #{key}]"
})

# if neither expiry is valid, set ttl to max
Redix.command(:redix, ["EXPIRE", key, @four_weeks_in_seconds])
end

# set expiry only if new expiry is greater than current (GT)
Expand Down
2 changes: 1 addition & 1 deletion lib/epochtalk_server_web/controllers/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ defmodule EpochtalkServerWeb.Controllers.Post do
"""
# TODO(akinsey): Implement for completion
# - parser
# - Implement guard in Validate that prevents passing in page and start at the same time
def by_thread(conn, attrs) do
# Parameter Validation
with thread_id <- Validate.cast(attrs, "thread_id", :integer, required: true),
page <- Validate.cast(attrs, "page", :integer, default: 1, min: 1),
start <- Validate.cast(attrs, "start", :integer, min: 1),
limit <- Validate.cast(attrs, "limit", :integer, default: 25, min: 1, max: 100),
desc <- Validate.cast(attrs, "desc", :boolean, default: true),
:ok <- Validate.mutually_exclusive!(attrs, ["page", "start"]),
user <- Guardian.Plug.current_resource(conn),
user_priority <- ACL.get_user_priority(conn),

Expand Down
2 changes: 0 additions & 2 deletions lib/epochtalk_server_web/controllers/thread.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ defmodule EpochtalkServerWeb.Controllers.Thread do
# TODO(akinsey): Post pre processing, authorizations, image processing, hooks. Things to consider:
# - does html sanitizer need to run on the front end too
# - how do we run the same parser/sanitizer on the elixir back end as the node frontend
# - processing mentions
# - does createImageReferences need to be called here? was this missing from the old code?
# - does updateUserActivity need to be called here? was this missing from the old code?
def create(conn, attrs) do
# authorization checks
with :ok <- ACL.allow!(conn, "threads.create"),
Expand Down
1 change: 1 addition & 0 deletions lib/epochtalk_server_web/errors/custom_errors.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ defmodule EpochtalkServerWeb.CustomErrors do
def exception(value) do
case value do
[] -> %InvalidPayload{}
[message: message] -> %InvalidPayload{message: message}
_ -> %InvalidPayload{message: gen_message(Enum.into(value, %{required: false}))}
end
end
Expand Down
Loading