Skip to content

Commit eba7381

Browse files
committed
[B] Correct authorizer discrepancies
* Remove errant `destroy` verb from authority ability mapping * Ensure that project creation is explicitly checked and authorized * Validate creating/updating journal issues for journal editors with an existing project * Correct typo in permission authorizer spec to actually check a permission record
1 parent 08b2e62 commit eba7381

File tree

15 files changed

+226
-77
lines changed

15 files changed

+226
-77
lines changed
Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
class PermissionAuthorizer < ApplicationAuthorizer
2-
3-
def self.default(_able, user, options = {})
4-
return true if editor_permissions?(user)
5-
return false unless options[:for]
6-
return false unless options[:for].respond_to? :permissions_manageable_by?
7-
8-
options[:for].permissions_manageable_by? user
9-
end
1+
# frozen_string_literal: true
102

3+
class PermissionAuthorizer < ApplicationAuthorizer
114
def default(_able, user, _options = {})
125
return true if creator_or_has_editor_permissions?(user, resource)
136
return false unless resource.resource
@@ -16,4 +9,13 @@ def default(_able, user, _options = {})
169
resource.resource.permissions_manageable_by? user
1710
end
1811

12+
class << self
13+
def default(_able, user, options = {})
14+
return true if editor_permissions?(user)
15+
return false unless options[:for]
16+
return false unless options[:for].respond_to? :permissions_manageable_by?
17+
18+
options[:for].permissions_manageable_by? user
19+
end
20+
end
1921
end

api/app/authorizers/project_authorizer.rb

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ class ProjectAuthorizer < ApplicationAuthorizer
2525
]
2626
end.freeze
2727

28+
# @note This should not actually be caught by any verbs for this model,
29+
# but we add it here to prevent the authorizer from short-circuiting
30+
# to the necessarily-permissive class-level {ProjectAuthorizer.default}.
31+
def default(_adjective, user, _options = {})
32+
# :nocov:
33+
has_any_role?(user, :admin)
34+
# :nocov:
35+
end
36+
37+
# Admins, editors, marketeers, and project creators can create projects.
38+
#
39+
# @param [User] user
40+
# @param [Hash] _options
41+
def creatable_by?(user, _options = {})
42+
has_any_role?(user, :admin, :editor, :marketeer, :project_creator)
43+
end
44+
2845
# First, we check to see if the project is a draft. If so, {#drafts_readable_by? it must be readable}.
2946
# Otherwise, we allow a project to be read.
3047
#
@@ -43,7 +60,7 @@ def readable_by?(user, options = {})
4360
# @param [User] user
4461
# @param [Hash] _options
4562
def updatable_by?(user, options = {})
46-
has_any_role?(user, :admin, :editor, :marketeer, :project_editor) || with_journal { |j| j.updatable_by? user, options}
63+
has_any_role?(user, :admin, :editor, :marketeer, :project_editor) || with_journal { |j| j.updatable_by? user, options }
4764
end
4865

4966
# @!group Project Property Access Control
@@ -173,7 +190,7 @@ def default(_adjective, _user, _options = {})
173190
#
174191
# @param [User] user
175192
# @param [Hash] _options
176-
def creatable_by?(user, options = {})
193+
def creatable_by?(user, _options = {})
177194
project_creator_permissions?(user) || user.journal_editor?
178195
end
179196

@@ -183,7 +200,7 @@ def creatable_by?(user, options = {})
183200
#
184201
# @param [User] user
185202
# @param [Hash] _options
186-
def deletable_by?(user, options = {})
203+
def deletable_by?(user, _options = {})
187204
has_any_role?(user, :admin, :editor, :project_editor)
188205
end
189206

api/app/controllers/api/v1/journal_issues_controller.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
# frozen_string_literal: true
2+
13
module API
24
module V1
35
# Journal Issues controller
46
class JournalIssuesController < ApplicationController
7+
include AuthorizesJSONAPIRelationships
58

69
resourceful! JournalIssue, authorize_options: { except: [:index, :show] } do
710
JournalIssue.filtered(
@@ -11,6 +14,8 @@ class JournalIssuesController < ApplicationController
1114
)
1215
end
1316

17+
before_action :maybe_enforce_project_access!, only: :update
18+
1419
def index
1520
@journal_issues = load_journal_issues
1621
render_multiple_resources @journal_issues, include: ["journal"]
@@ -34,7 +39,14 @@ def destroy
3439
@journal_issue.destroy
3540
end
3641

37-
protected
42+
private
43+
44+
# Check to make sure that the user is authorized to update the project, if present.
45+
#
46+
# @return [void]
47+
def maybe_enforce_project_access!
48+
authorize_jsonapi_relationship_in! journal_issue_params, Project
49+
end
3850

3951
def includes
4052
[:journal, :journal_volume, :project, "project.creators", "project.contributors",
@@ -51,7 +63,6 @@ def scope_visibility
5163
def scope_for_journal_issues
5264
JournalIssue.all
5365
end
54-
5566
end
5667
end
5768
end

api/app/controllers/api/v1/journals/relationships/journal_issues_controller.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1+
# frozen_string_literal: true
2+
13
module API
24
module V1
35
module Journals
46
module Relationships
57
class JournalIssuesController < AbstractJournalChildController
8+
include AuthorizesJSONAPIRelationships
69

710
resourceful! JournalIssue, authorize_options: { except: [:index] } do
811
JournalIssue.filtered(
912
with_pagination!(journal_issue_filter_params), scope: @journal.journal_issues.in_reverse_order, user: current_user
1013
)
1114
end
1215

16+
before_action :maybe_enforce_project_access!, only: :create
17+
1318
def index
1419
@journal_issues = load_journal_issues
1520
location = api_v1_journal_relationships_journal_issues_url(@journal.id)
@@ -19,24 +24,31 @@ def index
1924

2025
def create
2126
inputs = {}
27+
2228
inputs[:params] = journal_issue_params.to_h
2329
inputs[:creator] = current_user
2430
inputs[:journal] = @journal
2531

2632
authorize_action_for JournalIssue.new(journal: @journal)
2733

2834
outcome = JournalIssues::Create.run inputs
35+
2936
render_single_resource outcome.result
3037
end
3138

3239
private
3340

41+
# Check to make sure that the user is authorized to update the project, if present.
42+
# @return [void]
43+
def maybe_enforce_project_access!
44+
authorize_jsonapi_relationship_in! journal_issue_params, Project
45+
end
46+
3447
def location
3548
return "" unless @journal_issue.persisted?
3649

3750
api_v1_journal_issue_url(@journal_issue)
3851
end
39-
4052
end
4153
end
4254
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
module AuthorizesJSONAPIRelationships
4+
extend ActiveSupport::Concern
5+
6+
# Occasionally we need to authorize based on attributes in a relationship.
7+
#
8+
# This provides a standardized way to do so based on params constructed in {Validation}.
9+
#
10+
# @param [ActionController::Parameters, #dig] params
11+
# @param [Symbol] relationship_key
12+
# @param [Class<ApplicationRecord>] model_klass
13+
# @param [Symbol] action
14+
# @return [void]
15+
def authorize_jsonapi_relationship_in!(params, model_klass, action: :update, relationship_key: model_klass.model_name.i18n_key)
16+
id = params.dig(:data, :relationships, relationship_key, :data, :id)
17+
18+
record = load_jsonapi_relationship_for model_klass, id
19+
20+
# :nocov:
21+
return if record.blank?
22+
# :nocov:
23+
24+
Authority.enforce action, record, authority_user
25+
end
26+
27+
# @param [Class<ApplicationRecord>] model_klass
28+
# @param [String] id
29+
# @return [ApplicationRecord, nil]
30+
def load_jsonapi_relationship_for(model_klass, id)
31+
case id
32+
when model_klass then id
33+
when String then model_klass.find id
34+
end
35+
rescue ActiveRecord::RecordNotFound
36+
# intentionally left blank, we don't try to authorize non-existing records
37+
# and the failure should be caught elsewhere in the validation stack.
38+
end
39+
end
Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
1+
# frozen_string_literal: true
2+
13
module JournalIssues
24
class Create < ActiveInteraction::Base
3-
45
isolatable!
56

67
transactional!
78

89
record :creator, class: "User"
910
record :journal
11+
1012
# Params are filtered in strong parameters with journal_issue_params
1113
hash :params, strip: false
1214

1315
attr_reader :journal_issue
1416

1517
# @return [JournalIssue]
1618
def execute
17-
@journal_issue =
18-
::Updaters::Default.new(params).update(journal.journal_issues.new(creator: creator))
19+
@journal_issue = journal.journal_issues.new(creator: creator)
20+
21+
::Updaters::Default.new(params).update(@journal_issue)
1922

2023
create_and_assign_project unless @journal_issue.project.present?
2124

22-
journal_issue.save if journal_issue.valid?
25+
journal_issue.save
2326

2427
return journal_issue
2528
end
@@ -32,12 +35,12 @@ def create_and_assign_project
3235
title_parts.push "no. #{journal_issue.number}"
3336

3437
project = Project.create(title: title_parts.join(", "), creator: creator)
35-
Content::ScaffoldProjectContent.run project: project,
36-
configuration: {
37-
multiple_texts: true
38-
}
38+
39+
inputs = { project: project, configuration: { multiple_texts: true } }
40+
41+
compose Content::ScaffoldProjectContent, inputs
42+
3943
@journal_issue.project = project
4044
end
41-
4245
end
4346
end

api/config/initializers/authority.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
read: "readable",
4747
update: "updatable",
4848
delete: "deletable",
49-
destroy: "deleteable",
5049
read_deleted: "deleted_readable",
5150
read_drafts: "drafts_readable",
5251
fully_read: "fully_readable",

api/db/structure.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7373,3 +7373,5 @@ INSERT INTO "schema_migrations" (version) VALUES
73737373
('20250129200019'),
73747374
('20250210192150'),
73757375
('20250210230256');
7376+
7377+

api/spec/authorizers/comment_authorizer_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
the_subject_behaves_like "instance abilities", Comment, create: false, read: true, update: false, delete: false
1111

12-
the_subject_behaves_like "class abilities", Comment, create: false, read: true, update: false, destroy: false
12+
the_subject_behaves_like "class abilities", Comment, create: false, read: true, update: false, delete: false
1313
end
1414

1515
context "when the subject is an admin" do
@@ -34,7 +34,7 @@
3434

3535
the_subject_behaves_like "instance abilities", Comment, create: true, read: true, update: false, delete: false
3636

37-
the_subject_behaves_like "class abilities", Comment, create: true, read: true, update: true, destroy: true
37+
the_subject_behaves_like "class abilities", Comment, create: true, read: true, update: true, delete: true
3838

3939
end
4040

@@ -45,14 +45,14 @@
4545

4646
the_subject_behaves_like "instance abilities", Comment, create: false, read: true, update: false, delete: false
4747

48-
the_subject_behaves_like "class abilities", Comment, create: false, read: true, update: true, destroy: true
48+
the_subject_behaves_like "class abilities", Comment, create: false, read: true, update: true, delete: true
4949
end
5050
end
5151

5252
context "when the subject is the resource creator" do
5353
let_it_be(:subject, refind: true) { creator }
5454

55-
the_subject_behaves_like "instance abilities", Comment, read: true, update: true, destroy: true
55+
the_subject_behaves_like "instance abilities", Comment, read: true, update: true, delete: true
5656
end
5757

5858
context "when the comment is on an annotation in a closed project with disabled engagement" do
@@ -79,7 +79,7 @@
7979
subject.clear_email_confirmation!
8080
end
8181

82-
the_subject_behaves_like "instance abilities", Comment, create: false, read: true, update: true, destroy: true
82+
the_subject_behaves_like "instance abilities", Comment, create: false, read: true, update: true, delete: true
8383
end
8484
end
8585

api/spec/authorizers/export_target_authorizer_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
let(:user) { FactoryBot.create :user, :editor }
2222
let!(:export_target) { FactoryBot.create :export_target }
2323

24-
it { is_expected.to be_able_to(:read).on(export_target).and be_unable_to(:create, :update, :destroy).on(export_target) }
24+
it { is_expected.to be_able_to(:read).on(export_target).and be_unable_to(:create, :update, :delete).on(export_target) }
2525
end
2626
end

0 commit comments

Comments
 (0)