Skip to content

Commit 12d2680

Browse files
authored
Merge pull request #109 from Jess-White/feature/412-add-authorization
Feature/412 add authorization
2 parents 15fd4ff + 8198292 commit 12d2680

13 files changed

+318
-3
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,5 @@ gem 'jb'
5252
gem 'rack-cors'
5353

5454
gem 'ranked-model'
55+
56+
gem 'pundit', '~> 2.2'

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ GEM
159159
public_suffix (4.0.6)
160160
puma (4.3.12)
161161
nio4r (~> 2.0)
162+
pundit (2.2.0)
163+
activesupport (>= 3.0.0)
162164
racc (1.6.0)
163165
rack (2.2.4)
164166
rack-cors (1.1.1)
@@ -286,6 +288,7 @@ DEPENDENCIES
286288
jwt
287289
pg (>= 0.18, < 2.0)
288290
puma (~> 4.3.12)
291+
pundit (~> 2.2)
289292
rack-cors
290293
rails (~> 6.0.2, >= 6.0.2.1)
291294
ranked-model

app/controllers/api/invitations_controller.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ class InvitationsController < ApplicationController
55
before_action :ensure_organization_exists, except: [:accept]
66

77
def index
8+
authorize @organization, policy_class: InvitationPolicy
89
@invitations = @organization.pending_invitations
910
render 'index.json.jb'
1011
end
1112

1213
def create
14+
authorize @organization, policy_class: InvitationPolicy
1315
invitation_issuer = InvitationIssuer.new(create_invitation_params, @organization)
1416
@invitation = invitation_issuer.call!
1517
render 'show.json.jb', status: :created
1618
end
1719

1820
def accept
21+
authorize Invitation, :accept?
1922
# TODO: Remove /organizations/:id/users#create
2023
invitation_accepter = InvitationAccepter.new(params[:token], accept_invitation_user_params)
2124
@invitation = invitation_accepter.call!
@@ -27,15 +30,17 @@ def accept
2730
end
2831

2932
def reinvite
30-
invitation_email = Invitation.find(params[:id]).email
31-
invitation_issuer = InvitationIssuer.new({ email: invitation_email }, @organization)
33+
invitation = Invitation.find(params[:id])
34+
authorize invitation
35+
invitation_issuer = InvitationIssuer.new({ email: invitation.email }, @organization)
3236
@invitation = invitation_issuer.call!
3337

3438
render 'show.json.jb'
3539
end
3640

3741
def destroy
3842
@invitation = Invitation.find(params[:id])
43+
authorize @invitation
3944
@invitation.destroy!
4045

4146
render 'show.json.jb'

app/controllers/api/organization_users_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def show
2929

3030
def destroy
3131
@organization_user = organization_user
32+
authorize @organization_user
3233
@organization_user.destroy!
3334
render 'show.json.jb'
3435
end

app/controllers/application_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
# frozen_string_literal: true
22

33
class ApplicationController < ActionController::Base
4+
include Pundit::Authorization
5+
46
rescue_from JWT::DecodeError, with: :handle_unauthorized
57
rescue_from JWT::VerificationError, with: :handle_unauthorized
68
rescue_from JWT::ExpiredSignature, with: :handle_unauthorized
9+
rescue_from Pundit::NotAuthorizedError, with: :handle_unauthorized
710
rescue_from ActiveRecord::RecordNotFound, with: :handle_unauthorized
811
rescue_from ActiveRecord::RecordInvalid, with: :handle_record_invalid
912

app/policies/application_policy.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
class ApplicationPolicy
4+
attr_reader :user, :record
5+
6+
def initialize(user, record)
7+
@user = user
8+
@record = record
9+
end
10+
11+
def index?
12+
true
13+
end
14+
15+
def show?
16+
true
17+
end
18+
19+
def create?
20+
true
21+
end
22+
23+
def new?
24+
create?
25+
end
26+
27+
def update?
28+
true
29+
end
30+
31+
def edit?
32+
update?
33+
end
34+
35+
def destroy?
36+
true
37+
end
38+
39+
class Scope
40+
def initialize(user, scope)
41+
@user = user
42+
@scope = scope
43+
end
44+
45+
def resolve
46+
raise NotImplementedError, "You must define #resolve in #{self.class}"
47+
end
48+
49+
private
50+
51+
attr_reader :user, :scope
52+
end
53+
end

app/policies/invitation_policy.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
class InvitationPolicy < ApplicationPolicy
4+
# `record` is slightly different depending on which action we're authorizing
5+
# * for index? and create?, record is an organization instance
6+
# * for accept?, record is the symbol :accept?
7+
# * for reinvite? and destroy?, record is an invitation instance
8+
9+
def index?
10+
user.admin_of_organization?(record.id)
11+
end
12+
13+
def create?
14+
user.admin_of_organization?(record.id)
15+
end
16+
17+
def accept?
18+
true
19+
end
20+
21+
def reinvite?
22+
user.admin_of_organization?(record.organization.id)
23+
end
24+
25+
def destroy?
26+
user.admin_of_organization?(record.organization.id)
27+
end
28+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
class OrganizationUserPolicy < ApplicationPolicy
4+
def destroy?
5+
user.admin_of_organization?(record.organization.id)
6+
end
7+
end

spec/controllers/api/invitations_controller_spec.rb

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,38 @@
77
id created_at updated_at first_name last_name email expires_at
88
]
99

10-
let(:organization) { create(:organization) }
10+
let(:chidi) do
11+
create(:user, first_name: 'Chidi', last_name: 'Anagonye')
12+
end
13+
let(:shawn) do
14+
create(:user, first_name: 'Shawn')
15+
end
16+
let(:organization) { create(:organization, users: [chidi]) }
1117

1218
describe 'GET /organization/:organization_id/invitations' do
1319
let!(:invitations) do
1420
create_list(:invitation, 2, organization_id: organization.id)
1521
end
1622

23+
before do
24+
allow_any_instance_of(InvitationPolicy).to receive(:index?).and_return(true)
25+
end
26+
27+
context 'when invitation policy fails' do
28+
before do
29+
allow_any_instance_of(InvitationPolicy).to receive(:index?).and_return(false)
30+
end
31+
32+
it 'renders 401' do
33+
set_auth_header(chidi)
34+
get :index, params: { organization_id: organization.id }
35+
36+
expect(response).to have_http_status(401)
37+
end
38+
end
39+
1740
it 'renders 200 with organization invitations' do
41+
set_auth_header(chidi)
1842
get :index, params: { organization_id: organization.id }
1943

2044
expect(response).to have_http_status(200)
@@ -49,6 +73,23 @@
4973
}
5074
end
5175

76+
before do
77+
allow_any_instance_of(InvitationPolicy).to receive(:create?).and_return(true)
78+
end
79+
80+
context 'when invitation policy fails' do
81+
before do
82+
allow_any_instance_of(InvitationPolicy).to receive(:create?).and_return(false)
83+
end
84+
85+
it 'renders 401' do
86+
set_auth_header(chidi)
87+
post :create, params: invitation_params
88+
89+
expect(response).to have_http_status(401)
90+
end
91+
end
92+
5293
it 'render 422 with invalid or missing params' do
5394
post :create, params: {
5495
organization_id: organization.id,
@@ -87,6 +128,26 @@
87128
describe 'POST /invitations/:token/accept' do
88129
let!(:invitation) { create(:invitation, organization: organization) }
89130

131+
before do
132+
allow_any_instance_of(InvitationPolicy).to receive(:accept?).and_return(true)
133+
end
134+
135+
context 'when invitation policy fails' do
136+
before do
137+
allow_any_instance_of(InvitationPolicy).to receive(:accept?).and_return(false)
138+
end
139+
140+
it 'renders 401' do
141+
set_auth_header(chidi)
142+
post :accept, params: {
143+
token: invitation.token,
144+
password: 'password'
145+
}
146+
147+
expect(response).to have_http_status(401)
148+
end
149+
end
150+
90151
context 'when token has expired' do
91152
before do
92153
invitation.update!(expires_at: Date.yesterday)
@@ -163,6 +224,26 @@
163224
describe 'PATCH /organization/:organization_id/invitations/:id/reinvite' do
164225
let!(:invitation) { create(:invitation, organization: organization, expires_at: Date.current) }
165226

227+
before do
228+
allow_any_instance_of(InvitationPolicy).to receive(:reinvite?).and_return(true)
229+
end
230+
231+
context 'when invitation policy fails' do
232+
before do
233+
allow_any_instance_of(InvitationPolicy).to receive(:reinvite?).and_return(false)
234+
end
235+
236+
it 'renders 401' do
237+
set_auth_header(chidi)
238+
post :reinvite, params: {
239+
organization_id: invitation.organization.id,
240+
id: invitation.id
241+
}
242+
243+
expect(response).to have_http_status(401)
244+
end
245+
end
246+
166247
context 'when organization does not exist' do
167248
it 'renders 401' do
168249
post :reinvite, params: {
@@ -207,8 +288,28 @@
207288
end
208289

209290
describe 'DELETE /organization/:organization_id/invitations/:id' do
291+
before do
292+
allow_any_instance_of(InvitationPolicy).to receive(:destroy?).and_return(true)
293+
end
294+
210295
let(:invitation) { create(:invitation, organization: organization) }
211296

297+
context 'when invitation policy fails' do
298+
before do
299+
allow_any_instance_of(InvitationPolicy).to receive(:destroy?).and_return(false)
300+
end
301+
302+
it 'renders 401' do
303+
set_auth_header(chidi)
304+
delete :destroy, params: {
305+
organization_id: organization.id,
306+
id: invitation.id
307+
}
308+
309+
expect(response).to have_http_status(401)
310+
end
311+
end
312+
212313
context 'when organization does not exist' do
213314
it 'renders 401' do
214315
delete :destroy, params: {

spec/controllers/api/organization_users_controller_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,23 @@
207207
end
208208

209209
describe 'DELETE /organizations/:organization_id/users/:id' do
210+
before do
211+
allow_any_instance_of(OrganizationUserPolicy).to receive(:destroy?).and_return(true)
212+
end
213+
214+
context 'when organization user policy fails' do
215+
before do
216+
allow_any_instance_of(OrganizationUserPolicy).to receive(:destroy?).and_return(false)
217+
end
218+
219+
it 'renders 401' do
220+
set_auth_header(chidi)
221+
delete :destroy, params: { organization_id: good_place.id, id: chidi.id }
222+
223+
expect(response).to have_http_status(401)
224+
end
225+
end
226+
210227
it 'renders 401 if organization does not exist' do
211228
set_auth_header(chidi)
212229
delete :destroy, params: { organization_id: 123, id: chidi.id }

0 commit comments

Comments
 (0)