-
Notifications
You must be signed in to change notification settings - Fork 4k
/
article_policy.rb
245 lines (202 loc) · 8.96 KB
/
article_policy.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
class ArticlePolicy < ApplicationPolicy
MAX_TAG_LIST_SIZE = 4
# @return [TrueClass] when only Forem admins can post an Article.
# @return [FalseClass] when most any Forem user can post an Article.
#
# @note This is for Authorization System: use case 1-1. At present, this is the quickest way to
# refactor our code to deliver on that feature.
#
# @see https://github.com/forem/forem/pull/16437 for pattern of adding a predicate method to the
# "most relevant" class.
# @see https://github.com/orgs/forem/projects/46 for project details
def self.limit_post_creation_to_admins?
FeatureFlag.enabled?(:limit_post_creation_to_admins)
end
# @param query [Symbol] the name of one of the ArticlePolicy action predicates (e.g. :create?,
# :new?) though as a convenience, we will also accept :new, and :create.
# @return [TrueClass] if this query should default to hidden
# @return [FalseClass] if this query should not be hidden in the UI.
#
# @note The symmetry of the case statement structure with .scope_users_authorized_to_action
def self.include_hidden_dom_class_for?(query:)
case query.to_sym
when :create?, :new?, :create, :new
limit_post_creation_to_admins?
else
false
end
end
# Helps filter a `:users_scope` to those authorized to the `:action`. I want a list of all users
# who can create an Article. This policy method can help with that.
#
# @param users_scope [ActiveRecord::Relation] a scope for querying user objects
# @param action [Symbol] the name of one of the ArticlePolicy action predicates (e.g. :create?,
# :new?) though as a convenience, we will also accept :new, and :create.
#
# @return [ActiveRecord::Relation]
#
# @see https://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html#method-i-scope
#
# @note With this duplication it would be feasible to alter the instance method logics to use the
# class method (e.g. `ArticlePolicy.scope_authorized(users_scope: User, action:
# :create?).find_by(user.id)`) but that's a future consideration.
#
# @note This is not a Pundit scope (see https://github.com/varvet/pundit#scopes), as those methods
# are for answering "What articles can I see?" This method is for answering "Who all can
# <action> on Articles?"
#
# @note Why isn't this a User.scope method? Because the logic of who can take an action on the
# resource is the problem domain of the policy.
#
# @note The symmetry of the case statement structure with .include_hidden_dom_class_for?
def self.scope_users_authorized_to_action(users_scope:, action:)
case action.to_sym
when :create?, :new?, :create, :new
# Note the delicate dance to duplicate logic in a general sense. [I hope that] this is a
# stop-gap solution.
users_scope = users_scope.without_role(:suspended)
return users_scope unless limit_post_creation_to_admins?
# NOTE: Not a fan of reaching over to the constant of another class, but I digress.
users_scope.with_any_role(*Authorizer::RoleBasedQueries::ANY_ADMIN_ROLES)
else
# Not going to implement all of the use cases.
raise "Unhandled predicate: #{action} for #{self}.#{__method__}"
end
end
# @note [@jeremyf] I am re-implemnenting the initialize method, but removing the Pundit
# authorization. There's an assumption that all policy questions will require a user,
# unless you know specifically that they don't.
#
# @note as a reminder, if you attempt to authorize this policy in a controller with that calls
# {CachingHeaders#set_cache_control_headers} you may encounter some headaches. What do
# those headaches look like? When you call {CachingHeaders#set_cache_control_headers}, you
# are likely disallowing checks on current_user (via {EdgeCacheSafetyCheck#current_user}).
#
# @todo [@jeremyf] I don't like altering the initializer and its core assumption. But the other
# option to get Articles working for https://github.com/forem/forem/issues/16529 is to
# address the at present fundamental assumption regarding "Policies are for authorizing when
# you have a user, otherwise let the controller decide."
#
#
# @see even Rubocop thinks this is a bad idea. But the short-cut gets me unstuck. I hope there's
# enough breadcrumbs to undo this short-cut.
def initialize(user, record)
@user = user
@record = record
end
def feed?
true
end
# Does the user already have existing articles? Can they create an article?
#
# @return [TrueClass] They have existing published articles OR can create new ones.
# @return [FalseClass] They do not have published articles NOR can they create new ones.
#
# @note As part of our aspirations to only show users what is relevant to them and "hiding" what
# is not, this method will help us with the edge case of "should we show the user a
# dashboard listing of posts?"
#
# @note This handles the case in which a user has lost the ability to create posts (e.g. we've
# toggled on the feature limiting posts to admins only) but they have at least one published
# post. In that case we want to show them things like "their posts's analytics" or a
# dashboard of their published posts.
#
# @note This policy method is a bit different. It is strictly meant to return true or false.
# Other policies might raise exceptions, but the purpose of this method is for conditional
# rendering.
def has_existing_articles_or_can_create_new_ones?
require_user!
return true if user.articles.published.exists?
create?
rescue ApplicationPolicy::NotAuthorizedError
false
end
# @see {ArticlePolicy.scope_users_authorized_to_action} for "mirrored" details.
def create?
require_user_in_good_standing!
return true unless self.class.limit_post_creation_to_admins?
user_any_admin?
end
def update?
require_user_in_good_standing!
user_author? || user_super_admin? || user_org_admin? || user_any_admin?
end
def manage?
update? && record.published? && !record.scheduled?
end
def stats?
require_user!
user_author? || user_super_admin? || user_org_admin?
end
def subscriptions?
require_user!
user_author? || user_super_admin?
end
def elevated_user?
user_any_admin? || user_super_moderator?
end
# this method performs the same checks that determine:
# if the record can be featured
# if user can adjust any tag
# if user can perform moderator actions
def revoke_publication?
require_user!
return false unless @record.published?
elevated_user?
end
def allow_tag_adjustment?
require_user!
elevated_user? || tag_moderator_eligible?
end
def tag_moderator_eligible?
tag_ids_moderated_by_user = Tag.with_role(:tag_moderator, @user).ids
tag_ids_moderated_by_user.size.positive?
end
def destroy?
require_user!
user_author? || user_super_admin? || user_org_admin? || user_any_admin?
end
def moderate?
# Technically, we could check the limit_post_creation_to_admins? first, but [@jeremyf]'s
# operating on a "trying to maintain consistency" approach.
require_user_in_good_standing!
return false if self.class.limit_post_creation_to_admins?
# <2022-05-09 Mon> Don't let a user moderate their own article; though this may not be the desired behavior.
return false if user_author?
# Beware a trusted user does not guarantee that they are an admin. And more specifically, being
# an admin does not guarantee being trusted.
return true if user.trusted?
elevated_user?
end
alias admin_featured_toggle? revoke_publication?
alias toggle_featured_status? revoke_publication?
alias can_adjust_any_tag? revoke_publication?
alias can_perform_moderator_actions? revoke_publication?
# Due to the associated controller method "admin_unpublish", we
# alias "admin_ubpublish" to the "revoke_publication" method.
alias admin_unpublish? revoke_publication?
alias new? create?
alias delete_confirm? destroy?
alias discussion_lock_confirm? destroy?
alias discussion_unlock_confirm? destroy?
alias edit? update?
# The ArticlesController#preview method is very complicated but aspirationally, we want to ensure
# that someone can preview an article of their if they already have a published article or they
# can create new ones.
alias preview? has_existing_articles_or_can_create_new_ones?
def permitted_attributes
%i[title body_html body_markdown main_image published canonical_url
description tag_list publish_under_org
video video_code video_source_url video_thumbnail_url receive_notifications
archived]
end
private
def user_author?
# We might have the Article class (instead of the Article instance), so let's short circuit
return false unless record.respond_to?(:user_id)
record.user_id == user.id
end
def user_org_admin?
user.org_admin?(record.organization_id)
end
end