Skip to content

Commit 804b1b2

Browse files
committed
Fix project user membership assignment
1 parent 65ebe94 commit 804b1b2

File tree

6 files changed

+178
-67
lines changed

6 files changed

+178
-67
lines changed

config/dev.exs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use Mix.Config
2+
require Logger
23

34
# For development, we disable any cache and enable
45
# debugging and code reloading.
@@ -26,8 +27,7 @@ config :code_corps, CodeCorpsWeb.Endpoint,
2627
]
2728

2829
# Do not include metadata nor timestamps in development logs
29-
config :logger,
30-
:console, format: "[$level] $message\n"
30+
config :logger, :console, format: "[$level] $message\n"
3131

3232
# Set a higher stacktrace during development. Avoid configuring such
3333
# in production as building large stacktraces may be expensive.
@@ -54,11 +54,9 @@ config :code_corps, :analytics, CodeCorps.Analytics.InMemoryAPI
5454
config :code_corps, :stripe, Stripe
5555
config :code_corps, :stripe_env, :dev
5656

57-
config :sentry,
58-
environment_name: Mix.env || :dev
57+
config :sentry, environment_name: Mix.env() || :dev
5958

60-
config :code_corps, CodeCorps.Mailer,
61-
adapter: Bamboo.LocalAdapter
59+
config :code_corps, CodeCorps.Mailer, adapter: Bamboo.LocalAdapter
6260

6361
config :code_corps,
6462
postmark_forgot_password_template: "123",
@@ -69,7 +67,7 @@ config :code_corps,
6967
# If the dev environment has no CLOUDEX_API_KEY set, we want the app
7068
# to still run, with cloudex in test API mode
7169
if System.get_env("CLOUDEX_API_KEY") == nil do
72-
IO.puts("NOTE: No Cloudex configuration found. Cloudex is running in test mode.")
70+
Logger.info("NOTE: No Cloudex configuration found. Cloudex is running in test mode.")
7371
config :code_corps, :cloudex, CloudexTest
7472
config :cloudex, api_key: "test_key", secret: "test_secret", cloud_name: "test_cloud_name"
7573
end

lib/code_corps/policy/helpers.ex

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,50 @@ defmodule CodeCorps.Policy.Helpers do
55
"""
66

77
alias CodeCorps.{
8-
Organization, ProjectUser,
9-
Project, ProjectUser, Repo,
10-
TaskSkill, Task, User, UserTask
8+
Organization,
9+
ProjectUser,
10+
Project,
11+
ProjectUser,
12+
Repo,
13+
TaskSkill,
14+
Task,
15+
User,
16+
UserTask
1117
}
18+
1219
alias Ecto.Changeset
1320

1421
@doc """
1522
Determines if the provided organization or project is owned by the provided user
1623
"""
17-
@spec owned_by?(nil | Organization.t | Project.t, User.t) :: boolean
18-
def owned_by?(%Organization{owner_id: owner_id}, %User{id: user_id}),
19-
do: owner_id == user_id
24+
@spec owned_by?(nil | Organization.t() | Project.t(), User.t()) :: boolean
25+
def owned_by?(%Organization{owner_id: owner_id}, %User{id: user_id}), do: owner_id == user_id
26+
2027
def owned_by?(%Project{} = project, %User{} = user),
2128
do: project |> get_membership(user) |> get_role |> owner?
29+
2230
def owned_by?(nil, _), do: false
2331

2432
@doc """
2533
Determines if the provided project is being administered by the provided User
2634
2735
Returns `true` if the user is an admin or higher member of the project
2836
"""
29-
@spec administered_by?(nil | Project.t, User.t) :: boolean
37+
@spec administered_by?(nil | Project.t(), User.t()) :: boolean
3038
def administered_by?(%Project{} = project, %User{} = user),
3139
do: project |> get_membership(user) |> get_role |> admin_or_higher?
40+
3241
def administered_by?(nil, _), do: false
3342

3443
@doc """
3544
Determines if the provided project is being contributed to by the provided User
3645
3746
Returns `true` if the user is a contributor or higher member of the project
3847
"""
39-
@spec contributed_by?(nil | Project.t, User.t) :: boolean
48+
@spec contributed_by?(nil | Project.t(), User.t()) :: boolean
4049
def contributed_by?(%Project{} = project, %User{} = user),
4150
do: project |> get_membership(user) |> get_role |> contributor_or_higher?
51+
4252
def contributed_by?(nil, _), do: false
4353

4454
@doc """
@@ -47,10 +57,13 @@ defmodule CodeCorps.Policy.Helpers do
4757
4858
Returns `CodeCorps.Organization`
4959
"""
50-
@spec get_organization(struct | Changeset.t | any) :: Organization.t
60+
@spec get_organization(struct | Changeset.t() | any) :: Organization.t()
5161
def get_organization(%{"organization_id" => id}), do: Organization |> Repo.get(id)
5262
def get_organization(%{organization_id: id}), do: Organization |> Repo.get(id)
53-
def get_organization(%Changeset{changes: %{organization_id: id}}), do: Organization |> Repo.get(id)
63+
64+
def get_organization(%Changeset{changes: %{organization_id: id}}),
65+
do: Organization |> Repo.get(id)
66+
5467
def get_organization(_), do: nil
5568

5669
@doc """
@@ -59,7 +72,7 @@ defmodule CodeCorps.Policy.Helpers do
5972
6073
Returns `CodeCorps.Project`
6174
"""
62-
@spec get_project(struct | Changeset.t | any) :: Project.t
75+
@spec get_project(struct | Changeset.t() | any) :: Project.t()
6376
def get_project(%{"project_id" => id}), do: Project |> Repo.get(id)
6477
def get_project(%{project_id: id}), do: Project |> Repo.get(id)
6578
def get_project(%Changeset{changes: %{project_id: id}}), do: Project |> Repo.get(id)
@@ -68,27 +81,27 @@ defmodule CodeCorps.Policy.Helpers do
6881
@doc """
6982
Retrieves the role field from a `CodeCorps.ProjectUser` struct or an `Ecto.Changeset`
7083
"""
71-
@spec get_role(nil | ProjectUser.t | Changeset.t) :: String.t | nil
84+
@spec get_role(nil | ProjectUser.t() | Changeset.t()) :: String.t() | nil
7285
def get_role(nil), do: nil
7386
def get_role(%ProjectUser{role: role}), do: role
7487
def get_role(%Changeset{} = changeset), do: changeset |> Changeset.get_field(:role)
7588

76-
@spec admin_or_higher?(String.t) :: boolean
89+
@spec admin_or_higher?(String.t()) :: boolean
7790
defp admin_or_higher?(role) when role in ["admin", "owner"], do: true
7891
defp admin_or_higher?(_), do: false
7992

80-
@spec contributor_or_higher?(String.t) :: boolean
93+
@spec contributor_or_higher?(String.t()) :: boolean
8194
defp contributor_or_higher?(role) when role in ["contributor", "admin", "owner"], do: true
8295
defp contributor_or_higher?(_), do: false
8396

84-
@spec owner?(String.t) :: boolean
97+
@spec owner?(String.t()) :: boolean
8598
defp owner?("owner"), do: true
8699
defp owner?(_), do: false
87100

88101
@doc """
89102
Retrieves task from associated record
90103
"""
91-
@spec get_task(Changeset.t | TaskSkill.t | UserTask.t | map) :: Task.t
104+
@spec get_task(Changeset.t() | TaskSkill.t() | UserTask.t() | map) :: Task.t()
92105
def get_task(%TaskSkill{task_id: task_id}), do: Repo.get(Task, task_id)
93106
def get_task(%UserTask{task_id: task_id}), do: Repo.get(Task, task_id)
94107
def get_task(%{"task_id" => task_id}), do: Repo.get(Task, task_id)
@@ -97,12 +110,14 @@ defmodule CodeCorps.Policy.Helpers do
97110
@doc """
98111
Determines if the provided task was authored by the provided user
99112
"""
100-
@spec task_authored_by?(Task.t, User.t) :: boolean
113+
@spec task_authored_by?(Task.t(), User.t()) :: boolean
101114
def task_authored_by?(%Task{user_id: author_id}, %User{id: user_id}), do: user_id == author_id
102115

103-
# Returns `CodeCorps.ProjectUser` for specified `CodeCorps.Project`
104-
# and `CodeCorps.User`, or nil
105-
@spec get_membership(Project.t, User.t) :: nil | ProjectUser.t
106-
defp get_membership(%Project{id: project_id}, %User{id: user_id}),
116+
@doc """
117+
Returns `CodeCorps.ProjectUser` for specified `CodeCorps.Project`
118+
and `CodeCorps.User`, or nil
119+
"""
120+
@spec get_membership(Project.t(), User.t()) :: nil | ProjectUser.t()
121+
def get_membership(%Project{id: project_id}, %User{id: user_id}),
107122
do: ProjectUser |> Repo.get_by(project_id: project_id, user_id: user_id)
108123
end

lib/code_corps/policy/project_user.ex

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,83 @@ defmodule CodeCorps.Policy.ProjectUser do
22
@moduledoc """
33
Handles `User` authorization of actions on `ProjectUser` records
44
"""
5-
import CodeCorps.Policy.Helpers, only: [get_role: 1]
5+
6+
import CodeCorps.Policy.Helpers, only: [get_membership: 2, get_project: 1, get_role: 1]
67

78
alias CodeCorps.{ProjectUser, Repo, User}
89

9-
@spec create?(User.t, map) :: boolean
10-
def create?(%User{id: user_id}, %{"user_id" => author_id})
11-
when user_id == author_id and not is_nil(user_id), do: true
12-
def create?(%User{}, %{}), do: false
10+
@spec create?(User.t(), map) :: boolean
11+
def create?(%User{id: user_id}, %{"user_id" => author_id, "role" => "pending"})
12+
when user_id == author_id do
13+
# Non-member can only make pending if they're the user
14+
true
15+
end
16+
17+
def create?(%User{} = user, %{"user_id" => _, "project_id" => _} = params) do
18+
user_role =
19+
params
20+
|> get_project()
21+
|> get_membership(user)
22+
|> get_role()
23+
24+
new_role = Map.get(params, "role")
25+
do_create?(user_role, new_role)
26+
end
27+
28+
def create?(_, _), do: false
1329

14-
@spec update?(User.t, ProjectUser.t, map) :: boolean
30+
defp do_create?("pending", _), do: false
31+
defp do_create?("contributor", _), do: false
32+
defp do_create?("admin", "pending"), do: true
33+
defp do_create?("admin", "contributor"), do: true
34+
defp do_create?("admin", _), do: false
35+
defp do_create?("owner", _), do: true
36+
defp do_create?(_, _), do: false
37+
38+
@spec update?(User.t(), ProjectUser.t(), map) :: boolean
1539
def update?(%User{} = user, %ProjectUser{} = existing_record, params) do
16-
user_membership = existing_record |> get_project_membership(user)
40+
user_role =
41+
existing_record
42+
|> get_project_membership(user)
43+
|> get_role()
1744

18-
user_role = user_membership |> get_role
19-
old_role = existing_record |> get_role
45+
old_role = existing_record |> get_role()
2046
new_role = Map.get(params, "role")
2147

22-
case [user_role, old_role, new_role] do
23-
# Non-member, pending and contributors can't do anything
24-
[nil, _, _] -> false
25-
["pending", _, _] -> false
26-
["contributor", _, _] -> false
27-
# Admins can only approve pending memberships and nothing else
28-
["admin", "pending", "contributor"] -> true
29-
["admin", _, _] -> false
30-
# Owners can do everything expect changing other owners
31-
["owner", "owner", _] -> false
32-
["owner", _, _] -> true
33-
# No other role change is allowed
34-
[_, _, _] -> false
35-
end
48+
do_update?(user_role, old_role, new_role)
3649
end
3750

38-
@spec delete?(User.t, ProjectUser.t) :: boolean
51+
defp do_update?(nil, _, _), do: false
52+
defp do_update?("pending", _, _), do: false
53+
defp do_update?("contributor", _, _), do: false
54+
defp do_update?("admin", "pending", "contributor"), do: true
55+
defp do_update?("admin", _, _), do: false
56+
defp do_update?("owner", "owner", _), do: false
57+
defp do_update?("owner", _, _), do: true
58+
defp do_update?(_, _, _), do: false
59+
60+
@spec delete?(User.t(), ProjectUser.t()) :: boolean
3961
def delete?(%User{} = user, %ProjectUser{} = record) do
4062
record |> get_project_membership(user) |> do_delete?(record)
4163
end
4264

43-
defp do_delete?(%ProjectUser{} = user_m, %ProjectUser{} = current_m) when user_m == current_m, do: true
65+
defp do_delete?(%ProjectUser{} = user_m, %ProjectUser{} = current_m) when user_m == current_m,
66+
do: true
67+
4468
defp do_delete?(%ProjectUser{role: "owner"}, %ProjectUser{}), do: true
45-
defp do_delete?(%ProjectUser{role: "admin"}, %ProjectUser{role: role}) when role in ~w(pending contributor), do: true
69+
70+
defp do_delete?(%ProjectUser{role: "admin"}, %ProjectUser{role: role})
71+
when role in ~w(pending contributor),
72+
do: true
73+
4674
defp do_delete?(_, _), do: false
4775

4876
defp get_project_membership(%ProjectUser{user_id: nil}, %User{id: nil}), do: nil
49-
defp get_project_membership(%ProjectUser{user_id: m_id} = membership, %User{id: u_id}) when m_id == u_id, do: membership
77+
78+
defp get_project_membership(%ProjectUser{user_id: m_id} = membership, %User{id: u_id})
79+
when m_id == u_id,
80+
do: membership
81+
5082
defp get_project_membership(%ProjectUser{project_id: project_id}, %User{id: user_id}) do
5183
ProjectUser |> Repo.get_by(project_id: project_id, user_id: user_id)
5284
end

priv/repo/structure.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
-- PostgreSQL database dump
33
--
44

5-
-- Dumped from database version 10.0
6-
-- Dumped by pg_dump version 10.0
5+
-- Dumped from database version 9.5.10
6+
-- Dumped by pg_dump version 10.1
77

88
SET statement_timeout = 0;
99
SET lock_timeout = 0;

test/lib/code_corps/policy/project_user_test.exs

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,73 @@ defmodule CodeCorps.Policy.ProjectUserTest do
44
import CodeCorps.Policy.ProjectUser, only: [create?: 2, update?: 3, delete?: 2]
55

66
describe "create?/2" do
7-
test "returns true when user is creating their own membership" do
7+
test "when user is creating their own pending membership" do
88
user = insert(:user)
9+
project = insert(:project)
910

10-
assert create?(user, %{"user_id" => user.id})
11+
params = %{"project_id" => project.id, "user_id" => user.id, "role" => "pending"}
12+
assert create?(user, params)
1113
end
1214

13-
test "returns false for normal user, creating someone else's membership" do
15+
test "when user is creating any other membership" do
1416
user = insert(:user)
17+
project = insert(:project)
18+
19+
params = %{"project_id" => project.id, "user_id" => user.id, "role" => "contributor"}
20+
refute create?(user, params)
21+
end
22+
23+
test "when normal user is creating someone else's membership" do
24+
user = insert(:user)
25+
project = insert(:project)
26+
27+
params = %{"project_id" => project.id, "user_id" => "someone_else"}
28+
refute create?(user, params)
29+
end
30+
31+
test "when pending user is creating someone else's membership" do
32+
pending = insert(:user)
33+
project = insert(:project)
34+
insert(:project_user, role: "pending", user: pending, project: project)
35+
36+
params = %{"project_id" => project.id, "user_id" => "someone_else"}
37+
refute create?(pending, params)
38+
end
39+
40+
test "when contributor is creating someone else's membership" do
41+
contributor = insert(:user)
42+
project = insert(:project)
43+
insert(:project_user, role: "contributor", user: contributor, project: project)
44+
45+
params = %{"project_id" => project.id, "user_id" => "someone_else"}
46+
refute create?(contributor, params)
47+
end
48+
49+
test "when user is admin and role is contributor" do
50+
admin = insert(:user)
51+
project = insert(:project)
52+
insert(:project_user, role: "admin", user: admin, project: project)
53+
54+
params = %{"project_id" => project.id, "user_id" => "someone_else", "role" => "contributor"}
55+
assert create?(admin, params)
56+
end
57+
58+
test "when user is admin and role is admin" do
59+
admin = insert(:user)
60+
project = insert(:project)
61+
insert(:project_user, role: "admin", user: admin, project: project)
62+
63+
params = %{"project_id" => project.id, "user_id" => "someone_else", "role" => "admin"}
64+
refute create?(admin, params)
65+
end
66+
67+
test "when user is owner" do
68+
owner = insert(:user)
69+
project = insert(:project)
70+
insert(:project_user, role: "owner", user: owner, project: project)
1571

16-
refute create?(user, %{"user_id" => "someone_else"})
72+
params = %{"project_id" => project.id, "user_id" => "someone_else", "role" => "owner"}
73+
assert create?(owner, params)
1774
end
1875
end
1976

0 commit comments

Comments
 (0)