Skip to content

Commit 7c89c33

Browse files
authored
Merge pull request #735 from code-corps/717-change-project-permissions
Updated policies to rely on ProjectUser records
2 parents a5a4827 + 8ea2280 commit 7c89c33

32 files changed

+547
-1109
lines changed

lib/code_corps/helpers/policy.ex

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,82 +4,89 @@ defmodule CodeCorps.Helpers.Policy do
44
authorization policies.
55
"""
66

7-
import Ecto.Query
8-
97
alias CodeCorps.{
10-
Organization, OrganizationMembership,
11-
Project, ProjectUser, Repo, StripeConnectAccount,
8+
Organization, ProjectUser,
9+
Project, ProjectUser, Repo,
1210
TaskSkill, Task, User, UserTask
1311
}
1412
alias Ecto.Changeset
1513

1614
@doc """
17-
Retrieves the specified user's membership record, from a `CodeCorps.Project` struct or an `Ecto.Changeset`,
18-
containing an `organization_id` field, or from a `CodeCorps.Organization` struct
15+
Determines if the provided organization or project is owned by the provided user
16+
"""
17+
@spec owned_by?(nil | Organization.t | Project.t, User.t) :: boolean
18+
def owned_by?(%{owner_id: owner_id}, %User{id: user_id}), do: owner_id == user_id
19+
def owned_by?(nil, _), do: false
1920

20-
Returns `CodeCorps.OrganizationMembership`
21+
@doc """
22+
Determines if the provided project is being administered by the provided User
23+
24+
Returns `true` if
25+
- the user is the owner of the project
26+
- the user is an admin or higher member of the project
2127
"""
22-
@spec get_membership(nil | Changeset.t | Project.t | Organization.t | StripeConnectAccount.t, User.t) :: nil | OrganizationMembership.t
23-
def get_membership(nil, %User{}), do: nil
24-
def get_membership(%Changeset{changes: %{organization_id: organization_id}}, %User{id: user_id}), do: do_get_membership(organization_id, user_id)
25-
def get_membership(%Project{organization_id: organization_id}, %User{id: user_id}), do: do_get_membership(organization_id, user_id)
26-
def get_membership(%Organization{id: organization_id}, %User{id: user_id}), do: do_get_membership(organization_id, user_id)
27-
def get_membership(%StripeConnectAccount{organization_id: organization_id}, %User{id: user_id}), do: do_get_membership(organization_id, user_id)
28-
defp do_get_membership(organization_id, user_id) do
29-
OrganizationMembership
30-
|> where([m], m.member_id == ^user_id and m.organization_id == ^organization_id)
31-
|> Repo.one
28+
@spec administered_by?(nil | Project.t, User.t) :: boolean
29+
def administered_by?(%Project{} = project, %User{} = user) do
30+
case owned_by?(project, user) do
31+
true -> true
32+
false -> project |> get_membership(user) |> get_role |> admin_or_higher?
33+
end
3234
end
35+
def administered_by?(nil, _), do: false
3336

3437
@doc """
35-
Determines if the provided organization is owned by the provided user
38+
Determines if the provided project is being contributed to by the provided User
39+
40+
Returns `true` if
41+
- the user is the owner of the project
42+
- the user is a contributor or higher member of the project
3643
"""
37-
@spec organization_owned_by?(Organization.t, User.t) :: boolean
38-
def organization_owned_by?(%Organization{owner_id: owner_id}, %User{id: user_id}) do
39-
owner_id == user_id
44+
@spec contributed_by?(nil | Project.t, User.t) :: boolean
45+
def contributed_by?(%Project{} = project, %User{} = user) do
46+
case owned_by?(project, user) do
47+
true -> true
48+
false -> project |> get_membership(user) |> get_role |> contributor_or_higher?
49+
end
4050
end
51+
def contributed_by?(nil, _), do: false
52+
53+
@doc """
54+
Retrieves an organization record, from a model struct, or an `Ecto.Changeset`
55+
containing an `organization_id` field
56+
57+
Returns `CodeCorps.Organization`
58+
"""
59+
@spec get_organization(struct | Changeset.t | any) :: Organization.t
60+
def get_organization(%{organization_id: id}), do: Organization |> Repo.get(id)
61+
def get_organization(%Changeset{changes: %{organization_id: id}}), do: Organization |> Repo.get(id)
62+
def get_organization(_), do: nil
4163

4264
@doc """
43-
Retrieves a project record, from a model struct, or an `Ecto.Changeset` containing a `project_id` field
65+
Retrieves a project record, from a model struct, or an `Ecto.Changeset`
66+
containing a `project_id` field
4467
4568
Returns `CodeCorps.Project`
4669
"""
4770
@spec get_project(struct | Changeset.t | any) :: Project.t
48-
def get_project(%{project_id: project_id}), do: Project |> Repo.get(project_id)
49-
def get_project(%Changeset{changes: %{project_id: project_id}}), do: Project |> Repo.get(project_id)
71+
def get_project(%{project_id: id}), do: Project |> Repo.get(id)
72+
def get_project(%Changeset{changes: %{project_id: id}}), do: Project |> Repo.get(id)
5073
def get_project(_), do: nil
5174

5275
@doc """
53-
Retrieves the role field, from a `CodeCorps.OrganizationMembership` struct or an `Ecto.Changeset`
54-
55-
Returns `:string`
76+
Retrieves the role field from a `CodeCorps.ProjectUser` struct or an `Ecto.Changeset`
5677
"""
57-
@spec get_role(nil | OrganizationMembership.t | ProjectUser.t | Changeset.t) :: String.t | nil
78+
@spec get_role(nil | ProjectUser.t | Changeset.t) :: String.t | nil
5879
def get_role(nil), do: nil
59-
def get_role(%OrganizationMembership{role: role}), do: role
6080
def get_role(%ProjectUser{role: role}), do: role
6181
def get_role(%Changeset{} = changeset), do: changeset |> Changeset.get_field(:role)
6282

63-
@doc """
64-
Determines if provided string is equal to "owner"
65-
"""
66-
@spec owner?(String.t) :: boolean
67-
def owner?("owner"), do: true
68-
def owner?(_), do: false
69-
70-
@doc """
71-
Determines if provided string is equal to one of `["admin", "owner"]`
72-
"""
7383
@spec admin_or_higher?(String.t) :: boolean
74-
def admin_or_higher?(role) when role in ["admin", "owner"], do: true
75-
def admin_or_higher?(_), do: false
84+
defp admin_or_higher?(role) when role in ["admin", "owner"], do: true
85+
defp admin_or_higher?(_), do: false
7686

77-
@doc """
78-
Determines if provided string is equal to one of `["contributor", "admin", "owner"]`
79-
"""
8087
@spec contributor_or_higher?(String.t) :: boolean
81-
def contributor_or_higher?(role) when role in ["contributor", "admin", "owner"], do: true
82-
def contributor_or_higher?(_), do: false
88+
defp contributor_or_higher?(role) when role in ["contributor", "admin", "owner"], do: true
89+
defp contributor_or_higher?(_), do: false
8390

8491
@doc """
8592
Retrieves task from associated record
@@ -96,4 +103,9 @@ defmodule CodeCorps.Helpers.Policy do
96103
def task_authored_by?(%Task{user_id: author_id}, %User{id: user_id}), do: user_id == author_id
97104

98105

106+
# Returns `CodeCorps.ProjectUser` for specified `CodeCorps.Project`
107+
# and `CodeCorps.User`, or nil
108+
@spec get_membership(Project.t, User.t) :: nil | ProjectUser.t
109+
defp get_membership(%Project{id: project_id}, %User{id: user_id}),
110+
do: ProjectUser |> Repo.get_by(project_id: project_id, user_id: user_id)
99111
end

test/controllers/donation_goal_controller_test.exs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,21 @@ defmodule CodeCorps.DonationGoalControllerTest do
4444
describe "create" do
4545
@tag :authenticated
4646
test "creates and renders resource when data is valid", %{conn: conn, current_user: current_user} do
47-
organization = insert(:organization)
48-
insert(:organization_membership, member: current_user, organization: organization, role: "owner")
49-
project = insert(:project, organization: organization)
47+
project = insert(:project, owner: current_user)
5048

51-
attrs = @valid_attrs |> Map.merge(%{project: project})
49+
attrs = @valid_attrs |> Map.merge(%{project_id: project.id})
5250
assert conn |> request_create(attrs) |> json_response(201)
5351

5452
user_id = current_user.id
5553
assert_received {:track, ^user_id, "Created Donation Goal", %{}}
5654
end
5755

58-
@tag authenticated: :admin
59-
test "does not create resource and renders errors when data is invalid", %{conn: conn} do
60-
assert conn |> request_create(@invalid_attrs) |> json_response(422)
56+
@tag :authenticated
57+
test "does not create resource and renders errors when data is invalid", %{conn: conn, current_user: current_user} do
58+
project = insert(:project, owner: current_user)
59+
60+
attrs = @invalid_attrs |> Map.merge(%{project_id: project.id})
61+
assert conn |> request_create(attrs) |> json_response(422)
6162
end
6263

6364
test "renders 401 when not authenticated", %{conn: conn} do
@@ -73,22 +74,23 @@ defmodule CodeCorps.DonationGoalControllerTest do
7374
describe "update" do
7475
@tag :authenticated
7576
test "updates and renders chosen resource when data is valid", %{conn: conn, current_user: current_user} do
76-
organization = insert(:organization)
77-
insert(:organization_membership, member: current_user, organization: organization, role: "owner")
78-
project = insert(:project, organization: organization)
77+
project = insert(:project, owner: current_user)
7978

8079
donation_goal = insert(:donation_goal, project: project)
8180

82-
attrs = @valid_attrs |> Map.merge(%{project: project})
81+
attrs = @valid_attrs |> Map.merge(%{project_id: project.id})
8382
assert conn |> request_update(donation_goal, attrs) |> json_response(200)
8483

8584
user_id = current_user.id
8685
assert_received {:track, ^user_id, "Updated Donation Goal", %{}}
8786
end
8887

8988
@tag authenticated: :admin
90-
test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do
91-
assert conn |> request_update(@invalid_attrs) |> json_response(422)
89+
test "does not update chosen resource and renders errors when data is invalid", %{conn: conn, current_user: current_user} do
90+
project = insert(:project, owner: current_user)
91+
donation_goal = insert(:donation_goal, project: project)
92+
93+
assert conn |> request_update(donation_goal, @invalid_attrs) |> json_response(422)
9294
end
9395

9496
@tag authenticated: :admin
@@ -107,9 +109,10 @@ defmodule CodeCorps.DonationGoalControllerTest do
107109
end
108110

109111
describe "delete" do
110-
@tag authenticated: :admin
111-
test "deletes chosen resource", %{conn: conn} do
112-
donation_goal = insert(:donation_goal)
112+
@tag :authenticated
113+
test "deletes chosen resource", %{conn: conn, current_user: current_user} do
114+
project = insert(:project, owner: current_user)
115+
donation_goal = insert(:donation_goal, project: project)
113116
assert conn |> request_delete(donation_goal) |> response(204)
114117
end
115118

test/controllers/organization_membership_controller_test.exs

Lines changed: 0 additions & 136 deletions
This file was deleted.

test/controllers/project_category_controller_test.exs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,19 @@ defmodule CodeCorps.ProjectCategoryControllerTest do
4242
end
4343

4444
describe "create" do
45-
@tag authenticated: :admin
46-
test "creates and renders resource when data is valid", %{conn: conn} do
47-
organization = insert(:organization)
45+
@tag :authenticated
46+
test "creates and renders resource when data is valid", %{conn: conn, current_user: current_user} do
4847
category = insert(:category)
49-
project = insert(:project, organization: organization)
48+
project = insert(:project, owner: current_user)
5049

5150
attrs = %{category: category, project: project}
5251
assert conn |> request_create(attrs) |> json_response(201)
5352
end
5453

55-
@tag authenticated: :admin
56-
test "renders 422 when data is invalid", %{conn: conn} do
57-
invalid_attrs = %{}
54+
@tag :authenticated
55+
test "renders 422 when data is invalid", %{conn: conn, current_user: current_user} do
56+
project = insert(:project, owner: current_user)
57+
invalid_attrs = %{project: project}
5858
assert conn |> request_create(invalid_attrs) |> json_response(422)
5959
end
6060

@@ -69,9 +69,11 @@ defmodule CodeCorps.ProjectCategoryControllerTest do
6969
end
7070

7171
describe "delete" do
72-
@tag authenticated: :admin
73-
test "deletes resource", %{conn: conn} do
74-
assert conn |> request_delete |> response(204)
72+
@tag :authenticated
73+
test "deletes resource", %{conn: conn, current_user: current_user} do
74+
project = insert(:project, owner: current_user)
75+
project_category = insert(:project_category, project: project)
76+
assert conn |> request_delete(project_category) |> response(204)
7577
end
7678

7779
test "renders 401 when unauthenticated", %{conn: conn} do

0 commit comments

Comments
 (0)