Skip to content

Commit

Permalink
Merge pull request #349 from TechAndCheck/299-add-user-roles
Browse files Browse the repository at this point in the history
Add user roles
  • Loading branch information
reefdog authored Sep 20, 2022
2 parents 06f7949 + d27e0a8 commit af57aed
Show file tree
Hide file tree
Showing 27 changed files with 248 additions and 65 deletions.
6 changes: 5 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ AllCops:
# Join tables don't really need timestamps
Rails/CreateTableWithTimestamps:
Exclude:
# - 'db/migrate/20180128231930_create_organizations_and_events.rb'
- db/migrate/20220919194501_rolify_create_roles.rb

# Rails generates this file
Style/BlockComments:
Expand Down Expand Up @@ -64,6 +64,10 @@ Rails/HasManyOrHasOneDependent:

Rails/HasAndBelongsToMany:
Enabled: true
Exclude:
# Rolify uses HABTM. Despite a decade of the community attempting to implement
# `has_many: through`, it still struggles mightily with it. Let's make an exception.
- app/models/role.rb

Style/NumericPredicate:
Enabled: true
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,6 @@ gem "country_select", "~> 8.0"

# Used for sending email through Mailgun
gem "mailgun-ruby", "~> 1.2"

# Rolify is used for user roles
gem "rolify", "~> 6.0"
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rexml (3.2.5)
rolify (6.0.0)
rubocop (1.28.1)
parallel (~> 1.10)
parser (>= 3.1.0.0)
Expand Down Expand Up @@ -506,6 +507,7 @@ DEPENDENCIES
rails (~> 7.0.2.3)
rake
redis (~> 4.0)
rolify (~> 6.0)
rubocop
rubocop-minitest
rubocop-performance
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ def create
raise InvalidTokenError if @user.new_record?
raise InvalidUpdatePasswordError if typed_params.password.blank? || @user.invalid?

@user.remove_role :new_user

sign_in @user
redirect_to after_sign_in_path_for(@user)
end
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ def authenticate_super_user
# First we make sure they're logged in at all, this also sets the current user so we can check it
return false unless authenticate_user!

current_user.super_admin?
current_user.is_admin?
end

sig { void }
def authenticate_super_user!
# First we make sure they're logged in at all, this also sets the current user so we can check it
authenticate_user!

unless current_user.super_admin?
unless current_user.is_admin?
redirect_back_or_to "/", allow_other_host: false, alert: "You must be a super user/admin to access this page."
end
end
Expand Down
13 changes: 13 additions & 0 deletions app/models/role.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Role < ApplicationRecord
has_and_belongs_to_many :users, join_table: :users_roles

belongs_to :resource,
polymorphic: true,
optional: true

validates :resource_type,
inclusion: { in: Rolify.resource_types },
allow_nil: true

scopify
end
34 changes: 21 additions & 13 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# typed: strict

class User < ApplicationRecord
rolify
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:trackable, :lockable, :confirmable

has_many :api_keys, dependent: :delete_all
has_many :archive_items, foreign_key: :submitter_id, dependent: :nullify

Expand All @@ -9,17 +14,6 @@ class User < ApplicationRecord

has_one :applicant, dependent: :destroy

# Include default devise modules. Others available are:
# :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:trackable, :lockable, :confirmable

sig { returns(T::Boolean) }
def super_admin?
self.super_admin
end

# `Devise::Recoverable#set_reset_password_token` is a protected method, which prevents us from
# calling it directly. Since we need to be able to do that for tests and for duck-punching other
# `Devise::Recoverable` methods, we pull it into the public space here.
Expand All @@ -33,7 +27,7 @@ def set_reset_password_token
# Like the original method, it also creates the user's `reset_password_token`.
sig { returns(String) }
def send_setup_instructions
raise AlreadySetupError if sign_in_count.positive?
raise AlreadySetupError unless self.is_new_user?

token = set_reset_password_token

Expand All @@ -54,7 +48,7 @@ def send_setup_instructions
def self.create_from_applicant(applicant)
raise ApplicantNotApprovedError unless applicant.approved?

self.create!({
user = self.create!({
applicant: applicant,
email: applicant.email,
# The user will have to change their password immediately. This is just to pass validation.
Expand All @@ -64,6 +58,20 @@ def self.create_from_applicant(applicant)
confirmed_at: applicant.confirmed_at,
confirmation_sent_at: applicant.confirmation_sent_at
})

user.assign_default_roles

user
end

# All new users are implicitly Insights users.
# All new users are also "new" until they have completed their initial setup.
sig { void }
def assign_default_roles
if self.roles.blank?
self.add_role :new_user
self.add_role :insights_user
end
end
end

Expand Down
2 changes: 1 addition & 1 deletion app/views/layouts/_header.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<li>
<%= link_to "Settings", account_path, class: "block no-underline py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" %>
</li>
<% if current_user.super_admin? %>
<% if current_user.is_admin? %>
<li>
<%= link_to jobs_status_index_path, class: "flex flex-inline gap-1 block no-underline py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd" /></svg>
Expand Down
16 changes: 16 additions & 0 deletions config/initializers/rolify.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Rolify.configure do |config|
# By default ORM adapter is ActiveRecord. uncomment to use mongoid
# config.use_mongoid

# Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false
#
# Enabled because these are convenient methods, and according to the Rolify documentation they
# are generated at boot time (and when `add_role` is run), so shouldn't hurt performance.
config.use_dynamic_shortcuts

# Configuration to remove roles from database once the last resource is removed. Default is: true
#
# Toggled to false because we have well-defined user roles that we don't want removed, even if
# the last user using them is deleted.
config.remove_role_if_empty = false
end
18 changes: 18 additions & 0 deletions db/migrate/20220919194501_rolify_create_roles.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class RolifyCreateRoles < ActiveRecord::Migration[7.0]
def change
create_table "roles", id: :uuid do |t|
t.string :name
t.references :resource, polymorphic: true

t.timestamps
end

create_table "users_roles", id: false do |t|
t.references :user, type: :uuid
t.references :role, type: :uuid
end

add_index :roles, [ :name, :resource_type, :resource_id ]
add_index :users_roles, [ :user_id, :role_id ]
end
end
15 changes: 15 additions & 0 deletions db/migrate/20220919214503_convert_admins_to_rolify.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class ConvertAdminsToRolify < ActiveRecord::Migration[7.0]
def up
User.where(super_admin: true).each { |admin| admin.add_role :admin }

remove_column :users, :super_admin
end
def down
add_column :users, :super_admin, :boolean, default: false

User.with_role(:admin).each do |admin|
admin.update super_admin: true
admin.remove_role :admin
end
end
end
21 changes: 19 additions & 2 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 9 additions & 6 deletions db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
# Character.create(name: 'Luke', movie: movies.first)

Role.create!([
{ name: "new_user" },
{ name: "insights_user" },
{ name: "media_vault_user" },
{ name: "admin" },
])

easy_password = "password123"

# Super-admin account; no applicant necessary.
User.create!({
admin = User.create!({
email: "[email protected]",
password: easy_password,
super_admin: true,
confirmed_at: Time.now,
})
admin.add_role :admin

Applicant.create!([
# This applicant is a fresh, unconfirmed applicant.
Expand Down Expand Up @@ -84,8 +91,6 @@
# Override the randomized initial password.
password: easy_password,
password_confirmation: easy_password,
# Make sure they don't look fresh.
sign_in_count: 1,
})

# Create the restricted user
Expand All @@ -95,8 +100,6 @@
# Override the randomized initial password.
password: easy_password,
password_confirmation: easy_password,
# Make sure they don't look fresh.
sign_in_count: 1,
})

Sources::Tweet.create_from_url "https://twitter.com/kairyssdal/status/1415029747826905090"
Expand Down
9 changes: 8 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ Zenodotus allows its users to search its archive using image or text inputs. Sea

## User model

Zenodotus' `User` model handles authentication for the app via [Devise](https://github.com/heartcombo/devise). Internal users may be indicated as such with the `super_admin` boolean.
Zenodotus' `User` model handles authentication for the app via [Devise](https://github.com/heartcombo/devise). Roles are managed with the Rolify gem, which also generates role-specific helpers (e.g., `is_admin?`) at boot time.

Roles:

- `new_user`: Indicates a user was newly-created and has not yet gone through the setup process (clicked the link in their welcome email and chosen their own password). Applied to every new user created from an applicant, and removed when they have completed their own setup process.
- `insights_user`: Indicates a user has access to Fact-Check Insights. Applied to every new user created from an applicant.
- `media_vault_user`: Indicates a user has access to MediaVault. Currently only applied manually.
- `admin`: Indicates a user is authorized to administrate the site. (I.e., an internal user.)

## MediaReview
Coming soon
Expand Down
Loading

0 comments on commit af57aed

Please sign in to comment.