Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a UserAccess model #1242

Merged
merged 3 commits into from
Aug 25, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 0 additions & 53 deletions app/models/access.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
class Access < ApplicationRecord
ALLOWED_RESOURCES = %w[Organization FacilityGroup Facility].freeze
ACTION_TO_USER_ROLE = {
view: User.access_levels.fetch_values(:manager, :viewer),
manage: User.access_levels.fetch_values(:manager)
}

belongs_to :user
belongs_to :resource, polymorphic: true
Expand All @@ -13,55 +9,6 @@ class Access < ApplicationRecord
validates :resource, presence: true
validate :user_is_not_a_power_user, if: -> { user.present? }

class << self
def organizations(action)
resources_for(Organization, action)
end

def facility_groups(action)
resources_for(FacilityGroup, action)
.or(FacilityGroup.where(organization: organizations(action)))
end

def facilities(action)
resources_for(Facility, action)
.or(Facility.where(facility_group: facility_groups(action)))
end

def can?(action, model, record = nil)
if record&.is_a? ActiveRecord::Relation
raise ArgumentError, "record should not be an ActiveRecord::Relation."
end

case model
when :facility
can_access_record?(facilities(action), record)
when :organization
can_access_record?(organizations(action), record)
when :facility_group
can_access_record?(facility_groups(action), record)
else
raise ArgumentError, "Access to #{model} is unsupported."
end
end

private

def can_access_record?(resources, record)
return resources.find_by_id(record).present? if record
resources.exists?
end

def resources_for(resource_model, action)
resource_ids =
where(resource_type: resource_model.to_s)
.select { |access| access.user.access_level.in?(ACTION_TO_USER_ROLE[action]) }
.map(&:resource_id)

resource_model.where(id: resource_ids)
end
end

private

def user_is_not_a_power_user
Expand Down
42 changes: 12 additions & 30 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ class User < ApplicationRecord
allowed: "allowed",
denied: "denied"
}, _prefix: true
enum access_level: {
viewer: "viewer",
manager: "manager",
power_user: "power_user"
}, _suffix: :access
enum access_level: UserAccess::LEVELS.map { |level, meta| [level, meta[:id].to_s] }.to_h, _suffix: :access

belongs_to :organization, optional: true
has_many :user_authentications
Expand Down Expand Up @@ -77,6 +73,13 @@ class User < ApplicationRecord
:password,
:authenticatable_salt,
:invited_to_sign_up?, to: :email_authentication, allow_nil: true
delegate :accessible_organizations,
:accessible_facilities,
:accessible_facility_groups,
:can?,
:grant_access,
:access_tree,
:permitted_access_levels, to: :user_access, allow_nil: false

after_destroy :destroy_email_authentications

Expand All @@ -88,6 +91,10 @@ def email_authentication
email_authentications.first
end

def user_access
UserAccess.new(self)
end

def registration_facility_id
registration_facility.id
end
Expand Down Expand Up @@ -184,31 +191,6 @@ def resources
user_permissions.map(&:resource)
end

# #########################
# User Access (permissions)
#
def accessible_organizations(action)
return Organization.all if power_user?
accesses.organizations(action)
end

def accessible_facility_groups(action)
return FacilityGroup.all if power_user?
accesses.facility_groups(action)
end

def accessible_facilities(action)
return Facility.all if power_user?
accesses.facilities(action)
end

def can?(action, model, record = nil)
return true if power_user?
accesses.can?(action, model, record)
end
#
# #########################

def destroy_email_authentications
destroyable_email_auths = email_authentications.load

Expand Down
82 changes: 82 additions & 0 deletions app/models/user_access.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
class UserAccess < Struct.new(:user)
class NotAuthorizedError < StandardError; end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will come in handy later.


LEVELS = {
viewer: {
id: :viewer,
name: "View: Everything",
grant_access: [],
description: "Can view stuff"
},

manager: {
id: :manager,
name: "Manager",
grant_access: [:viewer, :manager],
description: "Can manage stuff"
},

power_user: {
id: :power_user,
name: "Power User",
description: "Can manage everything"
}
}.freeze

ACTION_TO_LEVEL = {
view: [:manager, :viewer],
manage: [:manager]
}.freeze

def accessible_organizations(action)
resources_for(Organization, action)
end

def accessible_facility_groups(action)
resources_for(FacilityGroup, action)
.or(FacilityGroup.where(organization: accessible_organizations(action)))
end

def accessible_facilities(action)
resources_for(Facility, action)
.or(Facility.where(facility_group: accessible_facility_groups(action)))
end

def can?(action, model, record = nil)
if record&.is_a? ActiveRecord::Relation
raise ArgumentError, "record should not be an ActiveRecord::Relation."
end

case model
when :facility
can_access_record?(accessible_facilities(action), record)
when :organization
can_access_record?(accessible_organizations(action), record)
when :facility_group
can_access_record?(accessible_facility_groups(action), record)
else
raise ArgumentError, "Access to #{model} is unsupported."
end
end

private

def can_access_record?(resources, record)
return true if user.power_user?
return resources.find_by_id(record).present? if record
resources.exists?
end

def resources_for(resource_model, action)
return resource_model.all if user.power_user?
return resource_model.none unless ACTION_TO_LEVEL.fetch(action).include?(user.access_level.to_sym)
Copy link
Contributor Author

@kitallis kitallis Aug 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clause has been moved up, it used to be like this before. It makes more sense this way since we're just pulling accesses off a concrete User.


resource_ids =
user
.accesses
.where(resource_type: resource_model.to_s)
.map(&:resource_id)

resource_model.where(id: resource_ids)
end
end
Loading