Analysis and remediation [likely] required for creating a Role system
Introduction
One of the refactoring chores necessary for a role system is to eschew the usage of method calls such as current_user.admin? in the view (or controller). These method calls are short-hand for implicit understandings of the code base. It's easy to fall into the habit of writing code that looks like the following:
if current_user.has_role?(:editor)
# render edit button
end
The above is a quick shortcut that sits as a proxy for the more robust implementation:
if can?(agent: current_user, action: edit, subject: @article)
# render edit button
end
What does remediation look like? It means assessing all of the places in which we ask something of the user. In the Places in Code to Explore and Remediate, I've identified candidates for consideration.
Places in Code to Explore and Remediate
app/controllers
- [ ] app/controllers/concerns/listings_toolkit.rb:74 calls
user.enough_credits?(cost) - [ ] app/controllers/concerns/listings_toolkit.rb:175 calls
user.enough_credits?(cost) - [ ] app/controllers/moderations_controller.rb:64 calls
user.has_role?(super_admin) - [ ] app/controllers/notifications/reads_controller.rb:9 calls
user.org_member?(params[org_id]) - [ ] app/controllers/async_info_controller.rb:62 calls
user.any_admin? - [ ] app/controllers/async_info_controller.rb:68 calls
user.creator? - [ ] app/controllers/api/v0/analytics_controller.rb:52 calls
user.org_member?(@org) - [ ] app/controllers/api/v0/articles_controller.rb:74 calls
user.has_role?(super_admin) - [ ] app/controllers/api/v0/articles_controller.rb:134 calls
user.org_admin?(params.dig("article", "organization_id") - [ ] app/controllers/api/v0/articles_controller.rb:135 calls
user.any_admin? - [ ] app/controllers/api/v0/api_controller.rb:38 calls
user.suspended? - [ ] app/controllers/api/v0/api_controller.rb:42 calls
user.has_role?(super_admin) - [x] app/controllers/users_controller.rb:55 calls
user.export_requested? - [x] app/controllers/users_controller.rb:85 calls
user.email? - [x] app/controllers/users_controller.rb:123 calls
user.email? - [ ] app/controllers/users_controller.rb:221 calls
user.org_admin?(org) - [ ] app/controllers/users_controller.rb:233 calls
user.org_admin?(org) - [ ] app/controllers/users_controller.rb:245 calls
user.org_admin?(org) - [ ] app/controllers/users_controller.rb:340 calls
user.authenticated_through?(github) - [ ] app/controllers/articles_controller.rb:288 calls
user.any_admin? - [ ] app/controllers/articles_controller.rb:294 calls
user.org_admin?(@article.organization_id) - [ ] app/controllers/admin/settings/base_controller.rb:35 calls
user.has_role?(super_admin) - [ ] app/controllers/admin/creator_settings_controller.rb:31 calls
user.has_role?(creator) - [ ] app/controllers/user_blocks_controller.rb:8 calls
user.blocking?(params[blocked_id].to_i) - [ ] app/controllers/credits_controller.rb:14 calls
user.org_admin?(params[organization_id]) - [ ] app/controllers/credits_controller.rb:24 calls
user.org_admin?(params[organization_id]) - [ ] app/controllers/dashboards_controller.rb:10 calls
user.org_admin?(params[org_id] || @user.any_admin?) - [ ] app/controllers/dashboards_controller.rb:14 calls
user.org_admin?(params[org_id]) - [ ] app/controllers/dashboards_controller.rb:14 calls
user.any_admin? - [ ] app/controllers/dashboards_controller.rb:82 calls
user.any_admin? - [x] app/controllers/omniauth_callbacks_controller.rb:109 calls
user.persisted? - [x] app/controllers/omniauth_callbacks_controller.rb:109 calls
user.valid? - [x] app/controllers/omniauth_callbacks_controller.rb:113 calls
user.persisted? - [ ] app/controllers/notifications_controller.rb:58 calls
user.admin? - [ ] app/controllers/notifications_controller.rb:86 calls
user.org_member?(params[org_id]) - [ ] app/controllers/notifications_controller.rb:86 calls
user.admin? - [ ] app/controllers/article_approvals_controller.rb:4 calls
user.any_admin? - [ ] app/controllers/reactions_controller.rb:97 calls
user.auditable? - [ ] app/controllers/reactions_controller.rb:170 calls
user.auditable? - [ ] app/controllers/tag_adjustments_controller.rb:36 calls
user.any_admin?
app/helpers
- [x] app/helpers/social_image_helper.rb:9 calls
user.is_a?(Organization)
app/models
- [ ] app/models/tag_adjustment.rb:26 calls
user.has_role?(tag_moderator, tag) - [ ] app/models/tag_adjustment.rb:27 calls
user.has_role?(admin) - [ ] app/models/tag_adjustment.rb:28 calls
user.has_role?(super_admin) - [x] app/models/badge_achievement.rb:47 calls
user.is_a?(User) - [x] app/models/github_repo.rb:38 calls
user.blank? - [ ] app/models/tweet.rb:115 calls
user.verified?
app/policies
- [ ] app/policies/internal_policy.rb:3 calls
user.has_any_role? - [ ] app/policies/tag_policy.rb:22 calls
user.has_role?(tag_moderator, record) - [ ] app/policies/liquid_tag_policy.rb:28 calls
user.has_role?(*Array(valid_role) - [ ] app/policies/github_repo_policy.rb:3 calls
user.authenticated_through?(github) - [ ] app/policies/github_repo_policy.rb:7 calls
user.authenticated_through?(github) - [ ] app/policies/comment_policy.rb:7 calls
user.comment_suspended? - [ ] app/policies/listing_policy.rb:11 calls
user.org_member?(record.organization_id) - [ ] app/policies/listing_policy.rb:29 calls
user.org_admin?(record.organization_id) - [ ] app/policies/article_policy.rb:64 calls
user.org_admin?(record.organization_id) - [ ] app/policies/organization_policy.rb:3 calls
user.suspended? - [ ] app/policies/organization_policy.rb:7 calls
user.org_admin?(record) - [ ] app/policies/organization_policy.rb:11 calls
user.org_admin?(record) - [ ] app/policies/application_policy.rb:63 calls
user.has_role?(super_admin) - [ ] app/policies/application_policy.rb:63 calls
user.has_role?(admin) - [ ] app/policies/application_policy.rb:67 calls
user.has_role?(super_admin) - [ ] app/policies/application_policy.rb:71 calls
user.has_role?(support_admin) - [ ] app/policies/application_policy.rb:77 calls
user.has_role?(trusted) - [ ] app/policies/user_policy.rb:102 calls
user.has_role?(trusted) - [ ] app/policies/user_policy.rb:102 calls
user.suspended?
app/services
- [ ] app/services/badges/award.rb:7 calls
user.banished? - [x] app/services/authentication/authenticator.rb:49 calls
user.nil? - [ ] app/services/slack/messengers/comment_user_warned.rb:21 calls
user.warned? - [ ] app/services/tag_moderators/add_trusted_role.rb:4 calls
user.has_role?(trusted) - [ ] app/services/tag_moderators/add_trusted_role.rb:4 calls
user.suspended? - [ ] app/services/users/approved_liquid_tags.rb:9 calls
user.has_role?(*Array(role) - [ ] app/services/users/update.rb:110 calls
user.suspended? - [ ] app/services/users/delete.rb:13 calls
user.suspended? - [x] app/services/re_captcha/check_enabled.rb:21 calls
user.nil? - [ ] app/services/re_captcha/check_enabled.rb:23 calls
user.tag_moderator? - [ ] app/services/re_captcha/check_enabled.rb:23 calls
user.any_admin? - [ ] app/services/re_captcha/check_enabled.rb:25 calls
user.suspended? - [ ] app/services/re_captcha/check_enabled.rb:28 calls
user.vomitted_on? - [x] app/services/articles/feeds/weighted_query_strategy.rb:321 calls
user.nil? - [x] app/services/articles/feeds/weighted_query_strategy.rb:541 calls
user.nil? - [x] app/services/articles/feeds/weighted_query_strategy.rb:553 calls
user.nil? - [ ] app/services/moderator/manage_activity_and_roles.rb:119 calls
user.suspended? - [ ] app/services/moderator/manage_activity_and_roles.rb:120 calls
user.warned? - [ ] app/services/moderator/manage_activity_and_roles.rb:121 calls
user.comment_suspended? - [x] app/services/moderator/banish_user.rb:18 calls
user.email? - [ ] app/services/mailchimp/bot.rb:70 calls
user.has_role?(trusted) - [ ] app/services/mailchimp/bot.rb:96 calls
user.tag_moderator? - [ ] app/services/mailchimp/bot.rb:148 calls
user.tag_moderator? - [x] app/services/broadcasts/welcome_notification/generator.rb:14 calls
user.subscribed_to_welcome_notifications?
app/views
- [ ] app/views/notifications/shared/_comment_box.html.erb:15 calls
user.has_role?(trusted) - [ ] app/views/moderations/mod.html.erb:22 calls
user.any_admin? - [ ] app/views/moderations/mod.html.erb:69 calls
user.has_role?(super_admin) - [ ] app/views/moderations/mod.html.erb:73 calls
user.has_role?(super_admin) - [ ] app/views/moderations/mod.html.erb:99 calls
user.any_admin? - [ ] app/views/moderations/mod.html.erb:139 calls
user.any_admin? - [ ] app/views/moderations/mod.html.erb:151 calls
user.has_role?(super_admin) - [ ] app/views/moderations/mod.html.erb:161 calls
user.any_admin? - [ ] app/views/moderations/actions_panel.html.erb:65 calls
user.any_admin? - [ ] app/views/moderations/actions_panel.html.erb:82 calls
user.any_admin? - [ ] app/views/moderations/actions_panel.html.erb:163 calls
user.any_admin? - [ ] app/views/users/show.html.erb:8 calls
user.suspended? - [ ] app/views/users/show.html.erb:86 calls
user.trusted? - [ ] app/views/users/_notifications.html.erb:16 calls
user.a_sustaining_member? - [ ] app/views/users/_notifications.html.erb:22 calls
user.tag_moderator? - [ ] app/views/users/_notifications.html.erb:28 calls
user.has_role?(trusted) - [ ] app/views/users/_additional_authentication.html.erb:1 calls
user.authenticated_with_all_providers? - [ ] app/views/users/_additional_authentication.html.erb:10 calls
user.authenticated_through?(provider.provider_name) - [ ] app/views/users/_account.html.erb:54 calls
user.export_requested? - [ ] app/views/users/_account.html.erb:110 calls
user.email? - [ ] app/views/videos/new.html.erb:21 calls
user.has_role?(super_admin) - [ ] app/views/tags/edit.html.erb:13 calls
user.has_role?(super_admin) - [ ] app/views/tags/edit.html.erb:13 calls
user.has_role?(admin) - [x] app/views/articles/feed.rss.builder:23 calls
user.instance_of?(User) - [ ] app/views/articles/manage.html.erb:36 calls
user.org_admin?(@article.organization) - [ ] app/views/admin/pages/_form.html.erb:77 calls
user.has_role?(tech_admin) - [ ] app/views/admin/users/show.html.erb:4 calls
user.registered? - [ ] app/views/admin/users/show.html.erb:10 calls
user.registered? - [ ] app/views/admin/users/show.html.erb:28 calls
user.access_locked? - [ ] app/views/admin/users/show.html.erb:44 calls
user.registered? - [ ] app/views/admin/users/edit.html.erb:7 calls
user.access_locked? - [ ] app/views/admin/users/edit.html.erb:23 calls
user.suspended? - [ ] app/views/admin/users/edit.html.erb:25 calls
user.warned? - [ ] app/views/admin/users/edit.html.erb:27 calls
user.comment_suspended? - [ ] app/views/admin/users/edit.html.erb:40 calls
user.has_role?(super_admin) - [ ] app/views/admin/users/edit.html.erb:135 calls
user.has_role?(super_admin) - [ ] app/views/admin/users/edit.html.erb:135 calls
user.has_role?(support_admin) - [ ] app/views/admin/users/edit.html.erb:150 calls
user.has_role?(super_admin) - [ ] app/views/admin/settings/show.html.erb:2 calls
user.has_role?(super_admin) - [ ] app/views/admin/settings/_update_setting_button.html.erb:1 calls
user.has_role?(super_admin) - [ ] app/views/devise/mailer/confirmation_instructions.html.erb:1 calls
user.creator?
References
For Forem core members see the following posts:
- Epic: Establish Roles systems to support limiting authoring and access permissions · Issue #301 · forem/rfcs
- Role System: Creating a Lexicon and WIP-ing up an Interface - Forem Team 🌱
Generating the list
I wrote the following Ruby script to populate the Places in Code to Explore and Remediate:
app_dirs = `ls #{ENV['HOME']}/git/forem/app`.split("\n")
app_dirs.each do |dir|
results = `cd #{ENV['HOME']}/git/forem; rg "user\\.\\w+\\?(\\([^\\)]\+\\))?" app/#{dir} --only-matching --line-number | awk -F":" '{ print "- [ ] [" $1 ":" $2 "](https://github.com/forem/forem/blob/50a73e035e2278bd0d52c2ab377822042028b3e3/" $1 "#L" $2 ") calls \`" $3 $4"\`"}'`
next if results.size.zero?
puts "### app/#{dir}\n\n"
puts results
puts "\n\n"
end
Thanks for the issue, we will take it into consideration! Our team of engineers is busy working on many types of features, please give us time to get back to you.
Feature requests that require more discussion may be closed. Read more about our feature request process on forem.dev.
To our amazing contributors: issues labeled type: bug are always up for grabs, but for feature requests, please wait until we add a ready for dev before starting to work on it.
To claim an issue to work on, please leave a comment. If you've claimed the issue and need help, please ping @forem/oss. The OSS Community Manager or the engineers on OSS rotation will follow up.
For full info on how to contribute, please check out our contributors guide.
So are you going to replace the use of current_user.posseses_or_can_do(some_role_or_ability)? with this:
if can?(agent: current_user, action: edit, subject: @article)
# render edit button
end
that part wasn't as clear to me
I don't yet know what I'm going to replace this with. I know we use Pundit, so there's a consideration of syntax.
I know we use Pundit, so there's a consideration of syntax.
I actually had typed up a comment to this effect yesterday, then decided to not post it because I thought it was too early in the process to discuss on that level of detail yet.
@citizen428 I got an email and was like "Yes! Remember to account for the existing system." My language comes from long time experience in CanCanCan, but I'm framing lots of things based on my work on robust roles/permission system (https://github.com/ndlib/sipity/blob/master/app/services/sipity/services/authorization_layer.rb)