Skip to content

Commit

Permalink
Groups with Roles feature
Browse files Browse the repository at this point in the history
Introduce ability to add roles to groups. Group members inherit the
permissions attached to the group's roles.

Notable changes:
- Several new Roles for Work, Collection, and User management
- Role permission integration with Hyrax permission logic (Sipity,
  access controls, CanCan, etc.)
- Hyku::Group replaced with already existing Hyrax::Group
- Easily assign roles to individual users
- Extended UI
- Both pre-configured and customizable groups
- Scripts to migrate existing Hyku applications
- More

This work is a joint contribution of PALNI and PALCI's Hyku for Consortia project.
  • Loading branch information
bkiahstroud committed Dec 13, 2022
1 parent e2f508f commit 1ba52f5
Show file tree
Hide file tree
Showing 185 changed files with 12,211 additions and 218 deletions.
6 changes: 5 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@ HYKU_MULTITENANT=true
# HYKU_MULTITENANT=false

# Uncomment this line to disable Bulkrax
# HYKU_BULKRAX_ENABLED=false
# HYKU_BULKRAX_ENABLED=false

# Comment this line if the Groups with Roles feature is disabled and to
# allow registered users to create curation concerns (Works, Collections, and FileSets)
SETTINGS__RESTRICT_CREATE_AND_DESTROY_PERMISSIONS=true
86 changes: 86 additions & 0 deletions GROUPS_WITH_ROLES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Groups with Roles

## Table of Contents
* [Creating Default Roles and Groups](#creating-default-roles-and-groups)
* [Setup an Existing Application to use Groups with Roles](#setup-an-existing-application-to-use-groups-with-roles)
* [Role Set Creation Guidelines](#role-set-creation-guidelines)
* [Other Information](#other-information)
* [Search Permissions Notes](#search-permissions-notes)

---

## Creating Default Roles and Groups

Default `Roles` and `Hyrax::Groups` are seeded into an account (tenant) at creation time (see [CreateAccount#create_defaults](app/services/create_account.rb)).

To manually seed default `Roles` and `Hyrax::Groups` _across all tenants_, run this rake task:

```bash
rake hyku:roles:create_default_roles_and_groups
```

## Setup an Existing Application to use Groups with Roles

These rake tasks will create data across all tenants necessary to setup Groups with Roles. **Run them in the order listed below.**

Prerequisites:
- All Collections must have CollectionTypes _and_ PermissionTemplates (see the **Collection Migration** section in the [Hyrax 2.1 Release Notes](https://github.com/samvera/hyrax/releases?after=v2.2.0))

```bash
rake hyku:roles:create_default_roles_and_groups
rake hyku:roles:create_collection_accesses
rake hyku:roles:create_admin_set_accesses
rake hyku:roles:create_collection_type_participants
rake hyku:roles:grant_workflow_roles
rake hyku:roles:destroy_registered_group_collection_type_participants # optional
```

<sup>\*</sup> The `hyku:roles:destroy_registered_group_collection_type_participants` task is technically optional. However, without it, collection readers will be allowed to create Collections.

## Role Set Creation Guidelines
1. Add role names to the [RolesService::DEFAULT_ROLES](app/services/roles_service.rb) constant
2. Find related ability concern in Hyrax (if applicable)
- Look in `app/models/concerns/hyrax/ability/` (local repo first, then Hyrax's repo)
- E.g. ability concern for Collections is `app/models/concerns/hyrax/ability/collection_ability.rb`
- If a concern matching the record type exists in Hyrax, but no the local repo, copy the file into the local repo
- Be sure to add override comments (use the `OVERRIDE:` prefix)
- If no concern matching the record type exists, create one.
- E.g. if creating an ablility concern for the `User` model, create `app/models/concerns/hyrax/ability/user_ability.rb`
3. Create a method in the concern called `<record_type>_roles` (e.g. `collection_roles`)
4. Add the method to the array of method names in [Ability#ability_logic](app/models/ability.rb`)
5. Within the `<record_type>_roles` method in the ability concern, add [CanCanCan](https://github.com/CanCanCommunity/cancancan) rules for each role, following that role's specific criteria.
- When adding/removing permissions, get as granular as possible.
- Beware using `can :manage` -- in CanCanCan, `:manage` [refers to **any** permission](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Defining-Abilities.md#the-can-method), not just CRUD actions.
- E.g. If you want a role to be able to _create_, _read_, _edit_, _update_, but not _destroy_ Users
```ruby
# Bad - could grant unwanted permissions
can :manage, User
cannot :destroy, User

# Good
can :create, User
can :read, User
can :edit, User
can :update, User
```
- CanCanCan rules are [hierarchical](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Ability-Precedence.md):
```ruby
# Will still grant read permission
cannot :manage, User # remove all permissions related to users
can :read, User
```
6. Add new / change existing `#can?` ability checks in views and controllers where applicable

### Other Information
- For guidelines on overriding dependencies, see the [Overrides to Dependencies](README#overrides-to-dependencies) section of the README
- Add [ability specs](spec/abilities) and [feature specs](spec/features)

## Search Permissions Notes
- Permissions are injected in the solr query's `fq` ("filter query") param ([link to code](https://github.com/projectblacklight/blacklight-access_controls/blob/master/lib/blacklight/access_controls/enforcement.rb#L56))
- Enforced (injected into solr query) in [Blacklight::AccessControls::Enforcement](https://github.com/projectblacklight/blacklight-access_controls/blob/master/lib/blacklight/access_controls/enforcement.rb)
- Represented by an instance of `Blacklight::AccessControls::PermissionsQuery` (see [#permissions_doc](https://github.com/projectblacklight/blacklight-access_controls/blob/master/lib/blacklight/access_controls/permissions_query.rb#L7-L14))
- Admin users don't have permission filters injected when searching ([link to code](https://github.com/samvera/hyrax/blob/v2.9.0/app/search_builders/hyrax/search_filters.rb#L15-L20))
- `SearchBuilder` may be related to when permissions are and aren't enforced
- Related discussions in Slack:
- [first inheritance question](https://samvera.slack.com/archives/C0F9JQJDQ/p1614103477032200)
- [second inheritance question](https://notch8.slack.com/archives/CD47U8QCQ/p1615935043012800)
65 changes: 65 additions & 0 deletions app/controllers/admin/group_roles_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module Admin
# OVERRIDE from AdminController inheretence for user roles authorization
class GroupRolesController < ApplicationController
before_action :load_group
before_action :cannot_remove_admin_role_from_admin_group, only: [:remove]
layout 'hyrax/dashboard'

rescue_from ActiveRecord::RecordNotFound, with: :redirect_not_found

def index
# OVERRIDE: AUTHORIZE AN EDIT ROLE TO ACCESS THE ROLES INDEX
authorize! :edit, Hyrax::Group
add_breadcrumb t(:'hyrax.controls.home'), root_path
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
add_breadcrumb t(:'hyku.admin.groups.title.edit'), edit_admin_group_path(@group)
add_breadcrumb t(:'hyku.admin.groups.title.roles'), request.path

@roles = ::Role.site - @group.roles
render template: 'admin/groups/roles'
end

def add
role = ::Role.find(params[:role_id])
@group.roles << role unless @group.roles.include?(role)

respond_to do |format|
format.html do
flash[:notice] = 'Role has successfully been added to Group'
redirect_to admin_group_roles_path(@group)
end
end
end

def remove
@group.group_roles.find_by!(role_id: params[:role_id]).destroy

respond_to do |format|
format.html do
flash[:notice] = 'Role has successfully been removed from Group'
redirect_to admin_group_roles_path(@group)
end
end
end

private

def load_group
@group = Hyrax::Group.find_by(id: params[:group_id])
end

def redirect_not_found
flash[:error] = 'Unable to find Group Role with that ID'
redirect_to admin_group_roles_path(@group)
end

def cannot_remove_admin_role_from_admin_group
role = Role.find_by(id: params[:role_id])
return unless @group.name == ::Ability.admin_group_name && role.name == 'admin'

redirect_back(fallback_location: edit_admin_group_path(@group), flash: { error: "Admin role cannot be removed from this group" })
end
end
end
15 changes: 13 additions & 2 deletions app/controllers/admin/group_users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# frozen_string_literal: true

module Admin
class GroupUsersController < AdminController
# OVERRIDE from AdminController inheretence for user roles authorization
class GroupUsersController < ApplicationController
before_action :load_group
before_action :cannot_remove_admin_users_from_admin_group, only: [:remove]
layout 'hyrax/dashboard'

def index
# OVERRIDE: AUTHORIZE AN EDIT ROLE TO ACCESS THE USERS INDEX
authorize! :edit, Hyrax::Group
add_breadcrumb t(:'hyrax.controls.home'), root_path
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
add_breadcrumb t(:'hyku.admin.groups.title.edit'), edit_admin_group_path(@group)
Expand All @@ -30,7 +35,7 @@ def remove
private

def load_group
@group = Hyku::Group.find_by(id: params[:group_id])
@group = Hyrax::Group.find_by(id: params[:group_id])
end

def page_number
Expand All @@ -40,5 +45,11 @@ def page_number
def page_size
params.fetch(:per, 10).to_i
end

def cannot_remove_admin_users_from_admin_group
return unless @group.name == ::Ability.admin_group_name

redirect_back(fallback_location: edit_admin_group_path(@group), flash: { error: "Admin users cannot be removed from this group" })
end
end
end
40 changes: 23 additions & 17 deletions app/controllers/admin/groups_controller.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
# frozen_string_literal: true

module Admin
class GroupsController < AdminController
before_action :load_group, only: %i[edit update remove destroy]
# OVERRIDE from AdminController inheretence for user roles authorization
class GroupsController < ApplicationController
# OVERRIDE: replaced before_action :load_group with the following load_and_authorize_resource
load_and_authorize_resource class: '::Hyrax::Group', instance_name: :group, only: %i[create edit update remove destroy]
layout 'hyrax/dashboard'

def index
# OVERRIDE: AUTHORIZE A READER ROLE TO ACCESS THE GROUPS INDEX
authorize! :read, Hyrax::Group
add_breadcrumb t(:'hyrax.controls.home'), root_path
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
add_breadcrumb t(:'hyku.admin.groups.title.index'), admin_groups_path
@groups = Hyku::Group.search(params[:q]).page(page_number).per(page_size)
@groups = Hyrax::Group.search(params[:q]).page(page_number).per(page_size)
end

def new
add_breadcrumb t(:'hyrax.controls.home'), root_path
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
add_breadcrumb t(:'hyku.admin.groups.title.new'), new_admin_group_path
@group = Hyku::Group.new
@group = Hyrax::Group.new
end

def create
new_group = Hyku::Group.new(group_params)
new_group = Hyrax::Group.new(group_params)
new_group.name = group_params[:humanized_name].gsub(" ", "_").downcase
if new_group.save
redirect_to admin_groups_path, notice: t('hyku.admin.groups.flash.create.success', group: new_group.name)
redirect_to admin_groups_path, notice: t('hyku.admin.groups.flash.create.success', group: new_group.humanized_name)
elsif new_group.invalid?
redirect_to new_admin_group_path, alert: t('hyku.admin.groups.flash.create.invalid')
else
Expand All @@ -36,11 +42,12 @@ def edit
end

def update
@group.name = group_params[:humanized_name].gsub(" ", "_").downcase
if @group.update(group_params)
redirect_to admin_groups_path, notice: t('hyku.admin.groups.flash.update.success', group: @group.name)
redirect_to admin_groups_path, notice: t('hyku.admin.groups.flash.update.success', group: @group.humanized_name)
else
redirect_to edit_admin_group_path(@group), flash: {
error: t('hyku.admin.groups.flash.update.failure', group: @group.name)
error: t('hyku.admin.groups.flash.update.failure', group: @group.humanized_name)
}
end
end
Expand All @@ -50,25 +57,24 @@ def remove
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
add_breadcrumb t(:'hyku.admin.groups.title.edit'), edit_admin_group_path
add_breadcrumb t(:'hyku.admin.groups.title.remove'), request.path

flash.now[:alert] = "Default groups cannot be destroyed." if @group.is_default_group?
end

def destroy
def destroy
return redirect_back(fallback_location: admin_groups_path) if @group.is_default_group?
if @group.destroy
redirect_to admin_groups_path, notice: t('hyku.admin.groups.flash.destroy.success', group: @group.name)
redirect_to admin_groups_path, notice: t('hyku.admin.groups.flash.destroy.success', group: @group.humanized_name)
else
logger.error("Hyku::Group id:#{@group.id} could not be destroyed")
redirect_to admin_groups_path flash: { error: t('hyku.admin.groups.flash.destroy.failure', group: @group.name) }
logger.error("Hyrax::Group id:#{@group.id} could not be destroyed")
redirect_to admin_groups_path flash: { error: t('hyku.admin.groups.flash.destroy.failure', group: @group.humanized_name) }
end
end

private

def load_group
@group = Hyku::Group.find_by(id: params[:id])
end

def group_params
params.require(:hyku_group).permit(:name, :description)
params.require(:group).permit(:name, :humanized_name, :description)
end

def page_number
Expand Down
27 changes: 27 additions & 0 deletions app/controllers/concerns/hyrax/admin/users_controller_behavior.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# OVERRIDE FILE from Hyrax v2.9.0
module Hyrax
module Admin
module UsersControllerBehavior
extend ActiveSupport::Concern
include Blacklight::SearchContext
included do
with_themed_layout 'dashboard'
end

# Display admin menu list of users
def index
# OVERRIDE: AUTHORIZE A READER ROLE TO ACCESS THE USERS INDEX
authorize! :read, ::User
add_breadcrumb t(:'hyrax.controls.home'), root_path
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
add_breadcrumb t(:'hyrax.admin.users.index.title'), hyrax.admin_users_path
@presenter = Hyrax::Admin::UsersPresenter.new
@invite_roles_options = if current_ability.admin?
::RolesService::DEFAULT_ROLES
else
::RolesService::DEFAULT_ROLES - [::RolesService::ADMIN_ROLE]
end
end
end
end
end
15 changes: 14 additions & 1 deletion app/controllers/concerns/hyrax/works_controller_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -478,8 +478,21 @@ def available_admin_sets
admin_sets = admin_set_results.map do |admin_set_doc|
template = templates.find { |temp| temp.source_id == admin_set_doc.id.to_s }

## OVERRIDE: Hyrax v3.4.2
# Removes a short-circuit that allowed users with manage access to
# the given permission_template to always be able to edit a record's sharing
# (i.e. the "Sharing" tab in forms).
#
# We remove this because there is currently a bug in Hyrax where, if the
# workflow does not allow access grants, changes to a record's sharing
# are not being persisted, leading to a confusing UX.
# @see https://github.com/samvera/hyrax/issues/5904
#
# TEMPORARY: This override should be removed when the bug is resolved in
# upstream Hyrax and brought into this project.
#
# determine if sharing tab should be visible
sharing = can?(:manage, template) || !!template&.active_workflow&.allows_access_grant? # rubocop:disable Style/DoubleNegation
sharing = !!template&.active_workflow&.allows_access_grant? # rubocop:disable Style/DoubleNegation

AdminSetSelectionPresenter::OptionsEntry
.new(admin_set: admin_set_doc, permission_template: template, permit_sharing: sharing)
Expand Down
1 change: 1 addition & 0 deletions app/controllers/hyku/invitations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def after_invite_path_for(_resource)
# override the standard invite so that accounts are added properly
# if they already exist on another tenant and invited if they do not
def create
authorize! :grant_admin_role, User if params[:user][:roles] == ::RolesService::ADMIN_ROLE
self.resource = User.find_by(email: params[:user][:email]) || invite_resource

# Set roles, whether they are a new user or not
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# OVERRIDE FILE from Hryax v2.9.0
require_dependency Hyrax::Engine.root.join('app', 'controllers', 'hyrax', 'admin', 'collection_type_participants_controller').to_s

Hyrax::Admin::CollectionTypeParticipantsController.class_eval do
before_action :admin_group_participant_cannot_be_destroyed, only: :destroy

# OVERRIDE: add backend validation to stop admin group access from being destroyed
def admin_group_participant_cannot_be_destroyed
@collection_type_participant = Hyrax::CollectionTypeParticipant.find(params[:id])
if @collection_type_participant.admin_group? && @collection_type_participant.access == Hyrax::CollectionTypeParticipant::MANAGE_ACCESS
redirect_to(
edit_admin_collection_type_path(@collection_type_participant.hyrax_collection_type_id, anchor: 'participants'),
alert: 'Admin group access cannot be removed'
)
end
end
private :admin_group_participant_cannot_be_destroyed
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# OVERRIDE FILE from Hryax v2.9.0
require_dependency Hyrax::Engine.root.join('app', 'controllers', 'hyrax', 'admin', 'permission_template_accesses_controller').to_s

Hyrax::Admin::PermissionTemplateAccessesController.class_eval do
# OVERRIDE: Only prevent delete if it is for the admin group's MANAGE access
#
# This is a controller validation rather than a model validation
# because we don't want to prevent the ability to remove the whole
# PermissionTemplate and all of its associated PermissionTemplateAccesses
# @return [Boolean] true if it's valid
def valid_delete?
# OVERRIDE: add MANAGE access condition
return true unless @permission_template_access.admin_group? && @permission_template_access.access == Hyrax::PermissionTemplateAccess::MANAGE

@permission_template_access.errors[:base] << t('hyrax.admin.admin_sets.form.permission_destroy_errors.admin_group')
false
end
end
Loading

0 comments on commit 1ba52f5

Please sign in to comment.