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

Allow Admins to Edit accesses #1252

Merged
merged 137 commits into from
Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
137 commits
Select commit Hold shift + click to select a range
d4ba4b6
Add the access model with some basic specs
kitallis Jul 28, 2020
798b3c3
WIP
kitallis Jul 28, 2020
1d3ef0d
Rename resource to scope
kitallis Jul 28, 2020
9839df3
Add a factory template for access
kitallis Jul 28, 2020
5338be1
Merge branch 'permissions-data-model' into permissions-access-methods
kitallis Jul 28, 2020
80bee5c
Add the three basic scoping methods
kitallis Jul 28, 2020
2b4b04b
Add more thorough validation specs
kitallis Jul 28, 2020
545a0bc
Merge branch 'permissions-data-model' into permissions-access-methods
kitallis Jul 28, 2020
03ba593
Rename ALLOWED_SCOPES to ALLOWED_RESOURCES
kitallis Jul 28, 2020
d6fdccd
Instead of inclusion:nil, use absence
kitallis Jul 29, 2020
d127930
Change mode to role
kitallis Jul 29, 2020
9406e31
Merge branch 'permissions-data-model' into permissions-access-methods
kitallis Jul 29, 2020
c6079ec
mode to role in factories
kitallis Jul 29, 2020
f21c824
Merge branch 'permissions-data-model' into permissions-access-methods
kitallis Jul 29, 2020
34ba637
Access.organizations specs
kitallis Jul 29, 2020
8fe631a
Access.organizations specs (pass 2)
kitallis Jul 29, 2020
69bc235
Rename mode to role in breaking spec
vkrmis Aug 3, 2020
8f35424
Merge branch 'permissions-data-model' into permissions-access-methods
vkrmis Aug 3, 2020
224f70a
Add a .can? method on User
vkrmis Aug 3, 2020
1e7b17e
Delegate user.can? to access model
vkrmis Aug 4, 2020
7ba5f87
Refactor the can? method for clarity and purpose
kitallis Aug 6, 2020
fdd518e
Add uniqueness constraint on user<>role
kitallis Aug 6, 2020
691d0cb
Add a validation for user to only have 1 role
kitallis Aug 6, 2020
4e5491b
Fix specs
kitallis Aug 6, 2020
7abeb1c
Specs to validate that user should only ever have one role thru Access
kitallis Aug 6, 2020
2482ea3
Minor aesthetic
kitallis Aug 6, 2020
4419d89
Add delegates to User for Access
kitallis Aug 6, 2020
a5f6210
Add specs to scope against FacilityGroups
kitallis Aug 6, 2020
344f119
Specs for Access.can?
kitallis Aug 6, 2020
8900c5a
Merge branch 'master' into permissions-access-methods
kitallis Aug 6, 2020
e9aa57a
Complete the pending Access spec
kitallis Aug 11, 2020
fdaccce
How it would look like if we moved the access_level to User
kitallis Aug 11, 2020
a7f32d8
Add react-checkbox-tree
kitallis Aug 12, 2020
1b99db9
Add a db constraint for access_level
kitallis Aug 13, 2020
75c8194
Get the build green
kitallis Aug 14, 2020
503749a
Add power_user validation in Access
kitallis Aug 14, 2020
91c1a81
Remove the power_user related specs from Access
kitallis Aug 14, 2020
05ece07
Wrap up the pending Access specs
kitallis Aug 14, 2020
b905b8e
Fix broken build
kitallis Aug 14, 2020
8c53a24
Merge branch 'master' into permissions-access-methods
kitallis Aug 17, 2020
7e9d5c3
Add a basic v2 of the InviteAdminForm
kitallis Aug 17, 2020
06d055e
Merge branch 'permissions-access-methods' into permissions-ui
kitallis Aug 17, 2020
592fafb
Fix bad merge
kitallis Aug 17, 2020
850ced1
Add checkbox and dropdowns to admin form
danySam Aug 17, 2020
4160776
Add jstree
kitallis Aug 17, 2020
45b465e
Add an InviteAdminPresenter layer to derive the access_tree for an admin
kitallis Aug 18, 2020
073da3c
Thanks standardrb
kitallis Aug 18, 2020
b8518c3
Add Facility Access nested and parent-aware checkboxes
kitallis Aug 18, 2020
572f862
Make the checkbox JS work with rails forms
kitallis Aug 19, 2020
a83f833
Change the datastructure for access_tree a bit
kitallis Aug 19, 2020
25bfa8a
Add some css
kitallis Aug 19, 2020
135757c
Rename input keys
kitallis Aug 19, 2020
53f96f2
On form submit, return the selected facilities
kitallis Aug 19, 2020
b1fad3f
Minor JS refactor
kitallis Aug 19, 2020
b2ad570
Add User#access_tree to eventually replace it in InviteAdminPresenter
kitallis Aug 19, 2020
688729d
Some notes from claudio
kitallis Aug 19, 2020
398c0c5
Add an access_control method for UI -> controller
kitallis Aug 19, 2020
80191aa
Add fuzzysort because I’d like to use this for search eventually
kitallis Aug 19, 2020
3ae4ea4
Move grant_access to User
kitallis Aug 20, 2020
cb7432e
Add spacer before checkboxes in tree
danySam Aug 20, 2020
e6ffd9b
Complete the basic, first horizontal invitation slice!
kitallis Aug 20, 2020
f388bfa
Make facility access card look like Figma design
danySam Aug 20, 2020
d72061e
Make facility access list collapsible
danySam Aug 20, 2020
bc7c7cf
Only allow grantable access_levels
kitallis Aug 20, 2020
a1e518b
Format new.html.erb
danySam Aug 21, 2020
5aa476c
Change hover color
danySam Aug 21, 2020
ccfe127
Merge branch 'permissions-ui' of https://github.com/simpledotorg/simp…
danySam Aug 21, 2020
4ac7060
Deprecate Presenter in favour of User#access_tree
kitallis Aug 21, 2020
ec74bfc
Save the user in a transaction
kitallis Aug 21, 2020
c82aa88
Better Access Level dropdowns
kitallis Aug 21, 2020
5dd89fb
Fix broken access level
kitallis Aug 21, 2020
a850629
Remove the unused React form
kitallis Aug 21, 2020
a7212f8
Merge branch 'master' into permissions-ui
kitallis Aug 21, 2020
7512692
Fix extra margin in CSS
danySam Aug 21, 2020
b03a6c3
Refactor grant_access
kitallis Aug 21, 2020
0f87863
Remove access-related bloat from User and move to Accessible
kitallis Aug 21, 2020
0749d23
Remove parent_id functions
kitallis Aug 21, 2020
1c178c4
Minor fixes
kitallis Aug 21, 2020
598e31a
Fix indeterminate state not being reset on top level check
danySam Aug 21, 2020
57ed207
Merge branch 'permissions-ui' of https://github.com/simpledotorg/simp…
danySam Aug 21, 2020
622b651
Cleanup invite_admin.js
danySam Aug 24, 2020
5d4a285
Code cleanup
danySam Aug 24, 2020
06f6849
Fix animation for item collapse
danySam Aug 24, 2020
45f620e
Remove the usage of JSON parsing in hidden input tag
danySam Aug 24, 2020
315a3d7
Move polyfills to separate JS file
danySam Aug 24, 2020
def2a09
Refactor: Cleanups names and functions calls
danySam Aug 24, 2020
730198e
Move all Access-related stuff to UserAccess and delegate from User
kitallis Aug 24, 2020
982fba3
Various refactors (sorry lazy to type this commit out)
kitallis Aug 25, 2020
59ce9ed
Various refactors (sorry lazy to type this commit out)
kitallis Aug 25, 2020
6ea60a4
Merge branch 'permissions-ui' of https://github.com/simpledotorg/simp…
danySam Aug 25, 2020
fd50227
Fix alignment issue in dropdown arrow
danySam Aug 25, 2020
7b34396
Use X / Y facilities language as in design
danySam Aug 25, 2020
201b48b
Merge branch 'master' into permissions-ui
kitallis Aug 25, 2020
be94c3a
Remove unused packages
kitallis Aug 25, 2020
5cfc0a0
Merge branch 'permissions-ui' of https://github.com/simpledotorg/simp…
danySam Aug 25, 2020
0fd8d26
Fix background and hover colors
danySam Aug 25, 2020
a3f9eda
Remove unused packages
kitallis Aug 25, 2020
8375e08
Small JS refactor
danySam Aug 25, 2020
739fb8c
Merge branch 'permissions-ui' of https://github.com/simpledotorg/simp…
danySam Aug 25, 2020
017e551
UI: Move dropdown button before text
danySam Aug 25, 2020
48a090b
Pull out the access_tree into a partial
kitallis Aug 25, 2020
a009653
Pull out some generic admin access view functions
kitallis Aug 25, 2020
8349963
Minor css reformat
kitallis Aug 25, 2020
0d35a98
Remove unnecessary changes
kitallis Aug 25, 2020
aaa567e
Refactor some JS and load it from within invite_admin.js
kitallis Aug 25, 2020
3a5c764
Show only total number of facilties if user has access to all
danySam Aug 25, 2020
4104f41
UI: Yellow background for items only when expanded
danySam Aug 25, 2020
cde3340
Minor refactor
kitallis Aug 25, 2020
efbfd71
Add specs for permitted_access_levels
kitallis Aug 25, 2020
b85c077
Minor refactor in InvitationsController
kitallis Aug 25, 2020
f571d0b
Add specs for UserAccess#access_tree
kitallis Aug 25, 2020
8993677
Add specs for UserAccess#grant_access
kitallis Aug 25, 2020
5014047
Add parallel methods for the EmailAuth -> Invite flow
kitallis Aug 26, 2020
bc0dde2
Remove yellow background from leaf facility item nodes
danySam Aug 26, 2020
1ff6e9b
Fix dropdown arrow not rotating
danySam Aug 26, 2020
5cac8b4
Add comments about the parallel methods
kitallis Aug 26, 2020
942fbcb
Thanks standard
kitallis Aug 26, 2020
c1c3846
Merge branch 'master' into permissions-ui
kitallis Aug 26, 2020
663be2c
The Add New Admin button is flipper flag aware
kitallis Aug 26, 2020
fcc3ca7
Pull the parallel dark launch methods back up into standard controllers
kitallis Aug 26, 2020
b12ec79
Remove the form submission checkbox hack and Just Use HTML™
kitallis Aug 26, 2020
5d4010e
Allow editing an existing admin
kitallis Aug 26, 2020
9f294a3
Minor JS cleanup
danySam Aug 27, 2020
5bbb361
Move dropdown arrow to the beginning of the checkbox item
danySam Aug 28, 2020
2d447c1
Fix dropdown toggle for collapse/expand
danySam Aug 28, 2020
75879e4
Remove extraneous JS
kitallis Aug 30, 2020
e075a59
Make the access tree more efficient
kitallis Aug 30, 2020
0809b7b
Delegate access_tree straight off the User
kitallis Aug 30, 2020
d188b7a
Make UserAccess#grant_access more efficient
kitallis Aug 30, 2020
b3db69e
Merge branch 'make-grant-access-more-efficient' into permissions-edit-ui
kitallis Aug 30, 2020
15744b8
Merge branch 'master' into permissions-edit-ui
kitallis Sep 2, 2020
d64cb26
Get edit, functional and merged properly
kitallis Sep 2, 2020
222394f
Add an “any” action
kitallis Sep 2, 2020
de41b6a
Standardrb
kitallis Sep 2, 2020
43efe5f
Simplify UserAccess#accessible_users
kitallis Sep 2, 2020
4a331be
Thanks standardrb
kitallis Sep 2, 2020
091ddf8
Merge branch 'master' into permissions-edit-ui
kitallis Sep 2, 2020
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
15 changes: 15 additions & 0 deletions app/assets/javascripts/invite_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,27 @@
// loads at page refresh
//
window.addEventListener("DOMContentLoaded", inviteAdmin);
window.addEventListener("DOMContentLoaded", editAdmin);


function inviteAdmin() {
checkboxItemListener()
resourceRowCollapseListener()
}

function editAdmin() {
const SELECTOR = "input.access-input"
const facilityAccessDiv = document.getElementById("facility-access")

// list of all checkboxes under facilityAccessDiv
const checkboxes = nodeListToArray(SELECTOR, facilityAccessDiv)
const checkedCheckboxes = checkboxes.filter(check => check.checked)

for (let checkbox of checkedCheckboxes) {
updateParentCheckedState(checkbox, SELECTOR)
}
}

//
// listeners
//
Expand Down
87 changes: 69 additions & 18 deletions app/controllers/admins_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ class AdminsController < AdminController
include Pagination
include SearchHelper

before_action :set_admin, only: [:show, :edit, :update, :destroy]
before_action :verify_params, only: [:update]
before_action :set_admin, only: [:show, :edit, :update, :destroy], unless: -> { Flipper.enabled?(:new_permissions_system_aug_2020, current_admin) }
before_action :🆕set_admin, only: [:show, :edit, :update], if: -> { Flipper.enabled?(:new_permissions_system_aug_2020, current_admin) }
before_action :verify_params, only: [:update], unless: -> { Flipper.enabled?(:new_permissions_system_aug_2020, current_admin) }
before_action :🆕verify_params, only: [:update], if: -> { Flipper.enabled?(:new_permissions_system_aug_2020, current_admin) }
after_action :verify_policy_scoped, only: :index
skip_after_action :verify_authorized, if: -> { Flipper.enabled?(:new_permissions_system_aug_2020, current_admin) }
skip_after_action :verify_policy_scoped, if: -> { Flipper.enabled?(:new_permissions_system_aug_2020, current_admin) }

def index
authorize([:manage, :admin, User])
Expand All @@ -22,23 +26,36 @@ def show
end

def edit
unless Flipper.enabled?(:new_permissions_system_aug_2020, current_admin)
authorize([:manage, :admin, current_admin])
end
end

def update
User.transaction do
@admin.update!(user_params)
next unless permission_params.present?

@admin.user_permissions.delete_all
permission_params.each do |attributes|
@admin.user_permissions.create!(attributes.permit(
:permission_slug,
:resource_id,
:resource_type
))
if Flipper.enabled?(:new_permissions_system_aug_2020, current_admin)
User.transaction do
@admin.update!(user_params)
current_admin.grant_access(@admin, selected_facilities)
end

redirect_to admins_url, notice: "Admin was successfully updated."
else
User.transaction do
@admin.update!(user_params)
next unless permission_params.present?

@admin.user_permissions.delete_all
permission_params.each do |attributes|
@admin.user_permissions.create!(attributes.permit(
:permission_slug,
:resource_id,
:resource_type
))
end
end

render json: {}, status: :ok
end
render json: {}, status: :ok
end

def destroy
Expand All @@ -57,19 +74,53 @@ def verify_params
end
end

#
# This is a temporary `verify_params` method that will exist until we migrate fully to the new permissions system
#
def 🆕verify_params
if selected_facilities.blank?
redirect_to edit_admin_path(@admin),
alert: "At least one facility should be selected for access before inviting an Admin."

return
end

@admin.assign_attributes(user_params)

if @admin.invalid?
redirect_to edit_admin_path,
alert: @admin.errors.full_messages.join("")
end
end

def set_admin
@admin = User.find(params[:id])
authorize([:manage, :admin, @admin])
end

def 🆕set_admin
@admin = authorize1 { current_admin.accessible_admins(:manage).find(params[:id]) }
end

def current_admin
AdminAccessPresenter.new(super)
end

def permission_params
params[:permissions]
end

def selected_facilities
params[:facilities].flatten
end

def user_params
{full_name: params[:full_name],
role: params[:role],
organization_id: params[:organization_id],
device_updated_at: Time.current}.compact
{
full_name: params[:full_name],
role: params[:role],
access_level: params[:access_level],
organization_id: params[:organization_id],
device_updated_at: Time.current
}.compact
end
end
11 changes: 9 additions & 2 deletions app/helpers/admin_access_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ def access_fraction(name, available, total)
# end
end

def access_checkbox(form, name, resource)
form.check_box(name, {id: resource.id, class: "access-input", label: resource.name.to_s}, resource.id, nil)
def access_checkbox(form, name, resource, page: :edit, checked_fn: -> { false })
opts = {
id: resource.id,
class: "access-input",
label: resource.name.to_s,
checked: page.eql?(:edit) && checked_fn.call
}

form.check_box("#{name}[]", opts, resource.id, nil)
end
end
3 changes: 3 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ def app_capabilities
->(term) { joins(:phone_number_authentications).merge(PhoneNumberAuthentication.search_by_phone(term)) }
scope :search_by_name_or_email, ->(term) { search_by_name(term).union(search_by_email(term)) }
scope :search_by_name_or_phone, ->(term) { search_by_name(term).union(search_by_phone(term)) }
scope :non_admins, -> { joins(:phone_number_authentications).where.not(phone_number_authentications: {id: nil}) }
scope :admins, -> { joins(:email_authentications).where.not(email_authentications: {id: nil}) }

validates :full_name, presence: true
validates :role, presence: true, if: -> { email_authentication.present? }
Expand Down Expand Up @@ -91,6 +93,7 @@ def app_capabilities
:accessible_facilities,
:accessible_facility_groups,
:accessible_users,
:accessible_admins,
:grant_access,
:permitted_access_levels, to: :user_access, allow_nil: false

Expand Down
31 changes: 22 additions & 9 deletions app/models/user_access.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class UserAccess < Struct.new(:user)
include Memery

class NotAuthorizedError < StandardError; end

class AuthorizationNotPerformedError < StandardError; end
Expand Down Expand Up @@ -39,35 +41,41 @@ class AuthorizationNotPerformedError < StandardError; end
}
}.freeze

ANY_ACTION = :any
ACTION_TO_LEVEL = {
manage_overdue_list: [:manager, :viewer_all, :call_center],
view_reports: [:manager, :viewer_all, :viewer_reports_only],
view_pii: [:manager, :viewer_all],
manage: [:manager]
}.freeze

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

def accessible_facility_groups(action)
memoize def accessible_facility_groups(action)
resources_for(FacilityGroup, action)
.union(FacilityGroup.where(organization: accessible_organizations(action)))
.includes(:organization)
end

def accessible_facilities(action)
memoize def accessible_facilities(action)
resources_for(Facility, action)
.union(Facility.where(facility_group: accessible_facility_groups(action)))
.includes(facility_group: :organization)
end

def accessible_users
facilities = accessible_facilities(:manage)
memoize def accessible_admins(action)
return User.admins if bypass?
return User.none if action_to_level(action).include?(:manage)

User.admins.where(organization: user.organization)
end

User.joins(:phone_number_authentications)
.where.not(phone_number_authentications: {id: nil})
.where(phone_number_authentications: {registration_facility_id: facilities})
memoize def accessible_users
User
.non_admins
.where(phone_number_authentications: {registration_facility_id: accessible_facilities(:manage)})
end

def permitted_access_levels
Expand Down Expand Up @@ -95,7 +103,7 @@ def grant_access(new_user, selected_facility_ids)

def resources_for(resource_model, action)
return resource_model.all if bypass?
return resource_model.none unless ACTION_TO_LEVEL.fetch(action).include?(user.access_level.to_sym)
return resource_model.none unless action_to_level(action).include?(user.access_level.to_sym)

resource_ids =
user
Expand Down Expand Up @@ -145,4 +153,9 @@ def prepare_grantable_resources(selected_facility_ids)
def bypass?
user.power_user?
end

def action_to_level(action)
ACTION_TO_LEVEL.values.flatten.uniq if action == ANY_ACTION
ACTION_TO_LEVEL[action]
end
end
53 changes: 33 additions & 20 deletions app/models/user_access_tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
class UserAccessTree < Struct.new(:user)
include Memery

def facilities
memoize def facilities
visible_facilities.map { |facility|
info = {
visible: true
Expand All @@ -16,45 +16,58 @@ def facilities
}.to_h
end

def facility_groups
memoize def facility_groups
facilities
.group_by { |facility, _| facility.facility_group }
.map { |facility_group, facilities|
info = {
accessible_facility_count: facilities.length,
visible: visible_facility_groups.include?(facility_group),
facilities: facilities
}
info = {
accessible_facility_count: facilities.length,
visible: visible_facility_groups.include?(facility_group),
facilities: facilities
}

[facility_group, info]
}.to_h
[facility_group, info]
}.to_h
end

def organizations
memoize def organizations
facility_groups
.group_by { |facility_group, _| facility_group.organization }
.map { |organization, facility_groups|
info = {
accessible_facility_count: facility_groups.sum { |_, info| info[:accessible_facility_count] },
visible: visible_organizations.include?(organization),
facility_groups: facility_groups
}
info = {
accessible_facility_count: facility_groups.sum { |_, info| info[:accessible_facility_count] },
visible: visible_organizations.include?(organization),
facility_groups: facility_groups
}

[organization, info]
}.to_h
[organization, info]
}.to_h
end

def visible?(model, record)
case model
when :facility
facilities.dig(record, :visible)
when :facility_group
facility_groups.dig(record, :visible)
when :organization
organizations.dig(record, :visible)
else
raise ArgumentError, "#{model} is unsupported."
end
end

private

memoize def visible_facility_groups
user.accessible_facility_groups(:view)
user.accessible_facility_groups(:any_access)
end

memoize def visible_facilities
user.accessible_facilities(:view)
user.accessible_facilities(:any_access)
end

memoize def visible_organizations
user.accessible_organizations(:view)
user.accessible_organizations(:any_access)
end
end
23 changes: 23 additions & 0 deletions app/views/admins/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
<% if Flipper.enabled?(:new_permissions_system_aug_2020, current_admin) %>
<div class="row">
<div class="col-md-6">
<%= bootstrap_form_with(url: admin_path(admin), method: 'PATCH', local: true, autocomplete: "off", label_errors: true) do |form| %>
<%= form.text_field :full_name, value: @admin.full_name, id: :full_name, required: true, autocomplete: "off", label: "Full Name *" %>
<%= form.text_field :email, value: @admin.email, id: :email, required: true, disabled: true, label: "Email *" %>
<%= form.text_field :role, value: @admin.role, id: :role, required: true, help: "CVHO, STS, State Official etc.", label: 'Job Title *' %>
<%= form.select :access_level, current_admin.permitted_access_levels_info, value: @admin.access_level, id: :access_levels, required: true, label: "Access *" %>

<%= form.label "Facility Access *" %>
<div id="facility-access">
<%= render "email_authentications/invitations/access_tree",
access_tree: current_admin.access_tree.organizations,
user_being_edited: AdminAccessPresenter.new(@admin),
page: :edit,
form: form %>
</div>
<%= form.primary("Save") %>
<% end %>
</div>
</div>
<% else %>
<%= javascript_include_tag "standalone/react_components" %>

<% user_permissions = current_admin.user_permissions.pluck(:permission_slug) %>
Expand All @@ -15,3 +37,4 @@
allow_email_edit: false,
submit_text: 'Update Admin',
submit_method: 'PATCH' %>
<% end %>
Loading