E1915 Authorization Utilities: Difference between revisions
(5 intermediate revisions by the same user not shown) | |||
Line 108: | Line 108: | ||
===New Implementation=== | ===New Implementation=== | ||
* We make use of the existing role.rb model method hasAllPrivileges of. This logic defines a hierarchy of users, allowing us to easily determine if the current user has a particular role "or above". We use this existing method to support the DRY principle, and to keep this logic in the model, where it belongs. We made one correction to this method, to change the logic from ">" to ">=", to ensure that for example a TA has all the privileges of a TA. | * We make use of the existing role.rb model ([https://github.com/AuroraTD/expertiza/blob/beta/app/models/role.rb link]) method hasAllPrivileges of. This logic defines a hierarchy of users, allowing us to easily determine if the current user has a particular role "or above". We use this existing method to support the DRY principle, and to keep this logic in the model, where it belongs. We made one correction to this method, to change the logic from ">" to ">=", to ensure that for example a TA has all the privileges of a TA. | ||
<pre> | <pre> | ||
def hasAllPrivilegesOf(target_role) | def hasAllPrivilegesOf(target_role) | ||
Line 164: | Line 164: | ||
** Add a new method to authorization_helper.rb ([https://github.com/AuroraTD/expertiza/blob/beta/app/helpers/authorization_helper.rb link]). | ** Add a new method to authorization_helper.rb ([https://github.com/AuroraTD/expertiza/blob/beta/app/helpers/authorization_helper.rb link]). | ||
** Comment the new method so that future developers can understand your work. | ** Comment the new method so that future developers can understand your work. | ||
** Add new tests covering your new method, to authorization_helper_spec.rb. | ** Add new tests covering your new method, to authorization_helper_spec.rb ([https://github.com/AuroraTD/expertiza/blob/beta/spec/helpers/authorization_helper_spec.rb link]). | ||
** Ensure that authorization_helper_spec.rb still passes with zero failures. | ** Ensure that authorization_helper_spec.rb ([https://github.com/AuroraTD/expertiza/blob/beta/spec/helpers/authorization_helper_spec.rb link]) still passes with zero failures. | ||
* What the authorization helper needs in order to work correctly: | * What the authorization helper needs in order to work correctly: | ||
Line 171: | Line 171: | ||
** The authorization helper needs users to be associated with roles. | ** The authorization helper needs users to be associated with roles. | ||
** The authorization helper needs roles to exist. | ** The authorization helper needs roles to exist. | ||
*** this is handled in spec_helper.rb | *** this is handled in spec_helper.rb ([https://github.com/AuroraTD/expertiza/blob/beta/spec/spec_helper.rb link]) | ||
** The authorization helper needs session[:user] to be populated with the current user. | ** The authorization helper needs session[:user] to be populated with the current user. | ||
*** this is handled in rails_helper.rb in the stub_current_user method | *** this is handled in rails_helper.rb ([https://github.com/AuroraTD/expertiza/blob/beta/spec/rails_helper.rb link]) in the stub_current_user method | ||
* Writing RSpec tests for new controllers (provide these needs): | * Writing RSpec tests for new controllers (provide these needs): | ||
** Use the factories defined in factories.rb to create objects to manipulate in your tests. | ** Use the factories defined in factories.rb ([https://github.com/AuroraTD/expertiza/blob/beta/spec/factories/factories.rb link]) to create objects to manipulate in your tests. | ||
*** factories were carefully designed (prior to the creation of the authorization helper) and we should take advantage of them | *** factories were carefully designed (prior to the creation of the authorization helper) and we should take advantage of them | ||
*** ensures that objects are more fully formed than would be the case with a simple double | *** ensures that objects are more fully formed than would be the case with a simple double | ||
Line 225: | Line 225: | ||
</pre> | </pre> | ||
* Write new comprehensive RSpec tests (authorization_helper_spec.rb) for every public method in our new helper (authorization_helper.rb). Run these tests, several times, to ensure that the new code works as intended. Please note that the output that you see may differ slightly, as more tests may be added. | * Write new comprehensive RSpec tests, in authorization_helper_spec.rb ([https://github.com/AuroraTD/expertiza/blob/beta/spec/helpers/authorization_helper_spec.rb link]), for every public method in our new helper, authorization_helper.rb ([https://github.com/AuroraTD/expertiza/blob/beta/app/helpers/authorization_helper.rb link]). Run these tests, several times, to ensure that the new code works as intended. Please note that the output that you see may differ slightly, as more tests may be added. | ||
<pre> | <pre> | ||
Line 249: | Line 249: | ||
</pre> | </pre> | ||
* The test suite for a single helper method is below. There are many such suites in authorization_helper_spec.rb. This example illustrates our general strategy: test missing input, test bad input, test various acceptable forms of input, test scenarios that lead to "true" and to "false" return values. | * The test suite for a single helper method is below. There are many such suites in authorization_helper_spec.rb ([https://github.com/AuroraTD/expertiza/blob/beta/spec/helpers/authorization_helper_spec.rb link]). This example illustrates our general strategy: test missing input, test bad input, test various acceptable forms of input, test scenarios that lead to "true" and to "false" return values. | ||
<pre> | <pre> |
Latest revision as of 10:55, 27 March 2019
E1915. Authorization Utilities
This page provides a description of an Expertiza OSS project.
About Expertiza
Expertiza is an open source project based on Ruby on Rails framework. Expertiza allows the instructor to create new assignments and customize new or existing assignments. It also allows the instructor to create a list of topics the students can sign up for. Students can form teams in Expertiza to work on various projects and assignments. Students can also peer review other students' submissions. Expertiza supports submission across various document types, including the URLs and wiki pages.
Problem Statement
The following tasks were accomplished in this project:
- Centralize user authentication logic to support the DRY principle
- Improve user authentication logic in cases where it was clearly flawed
- Support this work with RSpec unit tests
Current Implementation
Functionality
- Most controllers contain an action_allowed? method which determines which users are allowed to perform which actions
- This logic is in most cases correct, but is often repeated between controllers (un-DRY)
- This logic is in some cases slightly incorrect
- The Roles model provides a helpful method hasAllPrivilegesOf, which could be used to simplify authorization logic
Problems and Solutions
The problems listed below are examples of the four main classes of problems we encountered with Expertiza authorization. This is not an exhaustive list of problems, but is a good representation of the classes of problems addressed.
- Problem 1: Much of the authorization logic is repeated (un-DRY). For example, multiple controllers contain the following exact code.
['Super-Administrator', 'Administrator', 'Instructor', 'Teaching Assistant'].include? current_role_name
- Solution 1: Use one of the helper methods from the new authorization_helper.rb (link) to allow TAs *and above* (instructors, admins, super-admins) to perform this work.
current_user_has_ta_privileges?
- Problem 2: Some logic is slightly incorrect. For example, some places call for a specific user type, when users "above" this type should also be allowed to perform the work. In the following example (advertise_for_partner_controller.rb), only Students may advertise for partners. However per Dr. Gehringer, "There are no cases I am aware of where a particular type of user can do something that more-privileged users cannot do".
current_user.role.name.eql?("Student")
- Solution 2: Use one of the helper methods from the new authorization_helper.rb (link) to allow Students *and above* (TAs, instructors, admins, super-admins) to perform this work.
current_user_has_student_privileges?
- However, in case there IS a need to know if the current user has one specific role, this is still supported by the helper method current_user_is_a?
- Problem 3: Too much authorization logic is present in the controllers. This makes the controllers more difficult to read, and scatters authorization logic, when it would be easier to understand if it were all in one place.
def action_allowed? if %w[edit update list_submissions].include? params[:action] assignment = Assignment.find(params[:id]) (%w[Super-Administrator Administrator].include? current_role_name) || (assignment.instructor_id == current_user.try(:id)) || TaMapping.exists?(ta_id: current_user.try(:id), course_id: assignment.course_id) || (assignment.course_id && Course.find(assignment.course_id).instructor_id == current_user.try(:id)) else ['Super-Administrator', 'Administrator', 'Instructor', 'Teaching Assistant'].include? current_role_name end end
- Solution 3: Establish helper methods in the new authorization_helper.rb (link) to centralize as much authorization logic as possible. In this way, a developer with questions about authorization knows just where to look to find answers - authorization_helper.rb (link).
def action_allowed? if %w[edit update list_submissions].include? params[:action] current_user_has_admin_privileges? || current_user_teaching_staff_of_assignment?(params[:id]) else current_user_has_ta_privileges? end end
- Problem 4: Some action_allowed? methods are difficult to follow, and/or knowledge about how the action parameter should affect authorization is buried in another method.
def action_allowed? current_user_has_student_privileges?and ((%w[edit].include? action_name) ? are_needed_authorizations_present?(params[:id], "reader", "reviewer") : true) and one_team_can_submit_work? end
- Solution 4: Clean up action_allowed? methods and make the influence of the action parameter visible at this level.
def action_allowed? case params[:action] when 'edit' current_user_has_student_privileges? && are_needed_authorizations_present?(params[:id], "reader", "reviewer") when 'submit_file', 'submit_hyperlink' current_user_has_student_privileges? && one_team_can_submit_work? else current_user_has_student_privileges? end end
New Implementation
- We make use of the existing role.rb model (link) method hasAllPrivileges of. This logic defines a hierarchy of users, allowing us to easily determine if the current user has a particular role "or above". We use this existing method to support the DRY principle, and to keep this logic in the model, where it belongs. We made one correction to this method, to change the logic from ">" to ">=", to ensure that for example a TA has all the privileges of a TA.
def hasAllPrivilegesOf(target_role) privileges = {} privileges["Student"] = 1 privileges["Teaching Assistant"] = 2 privileges["Instructor"] = 3 privileges["Administrator"] = 4 privileges["Super-Administrator"] = 5 privileges[self.name] >= privileges[target_role.name] end
- We establish several methods in authorization_helper.rb (link) to expose easy-to-read method names for use in controllers.
def current_user_has_super_admin_privileges? ... def current_user_has_admin_privileges? ... def current_user_has_instructor_privileges? ... def current_user_has_ta_privileges? ... def current_user_has_student_privileges? ...
- We establish a method in authorization_helper.rb (link) to expose an easy-to-read method for determining if the current user "is a" [particular user role]. This is used in a minority of cases, because most logic cares if the current user "is a" [particular role] "or above".
def current_user_is_a?(role_name) ...
- We establish several methods in authorization_helper.rb (link) to centralize more complex authorization logic so that it is not scattered among controllers, but rather is kept in the same helper file as other authorization logic. Only a few of these methods are shown below.
def current_user_is_assignment_participant?(assignment_team_id) ... def current_user_teaching_staff_of_assignment?(assignment_id) ... def current_user_created_bookmark_id?(bookmark_id) ...
Using Authorization Helper Methods in a New Controller
- Add "include AuthorizationHelper" at the top of the controller definition.
- Look through authorization_helper.rb (link) for the method(s) you need.
- If you find the method definition comments lacking, please add to them.
- If you do not find a method you need:
- What the authorization helper needs in order to work correctly:
- The authorization helper needs users to have IDs.
- The authorization helper needs users to be associated with roles.
- The authorization helper needs roles to exist.
- this is handled in spec_helper.rb (link)
- The authorization helper needs session[:user] to be populated with the current user.
- this is handled in rails_helper.rb (link) in the stub_current_user method
- Writing RSpec tests for new controllers (provide these needs):
- Use the factories defined in factories.rb (link) to create objects to manipulate in your tests.
- factories were carefully designed (prior to the creation of the authorization helper) and we should take advantage of them
- ensures that objects are more fully formed than would be the case with a simple double
- specifically, ensures that created users have roles
- Use the create(:some_factory) style.
- ensures that the created object has an ID
- Use the stub_current_user method.
- ensures that session[:user] is populated
- Explicitly set session[:user] to nil if you need to simulate the total lack of any logged-in user for a test.
- Use the factories defined in factories.rb (link) to create objects to manipulate in your tests.
Automated Testing with RSPEC
Our strategy for gaining confidence that our code changes did not break anything was as follows:
- Run all existing controller RSpec tests after our changes, several times, to ensure that we have not introduced any failures.
e@ubuntu:~/Desktop/expertiza$ bundle exec rspec spec/controllers/ [...] Randomized with seed 12980 [...] 277 examples, 0 failures, 1 pending [...] e@ubuntu:~/Desktop/expertiza$ bundle exec rspec spec/controllers/ [...] Randomized with seed 17577 [...] 277 examples, 0 failures, 1 pending [...] e@ubuntu:~/Desktop/expertiza$ bundle exec rspec spec/controllers/ [...] Randomized with seed 65460 [...] 277 examples, 0 failures, 1 pending [...] e@ubuntu:~/Desktop/expertiza$ bundle exec rspec spec/controllers/ [...] Randomized with seed 12118 [...] 277 examples, 0 failures, 1 pending [...] e@ubuntu:~/Desktop/expertiza$ bundle exec rspec spec/controllers/ [...] Randomized with seed 8946 [...] 277 examples, 0 failures, 1 pending
- Write new comprehensive RSpec tests, in authorization_helper_spec.rb (link), for every public method in our new helper, authorization_helper.rb (link). Run these tests, several times, to ensure that the new code works as intended. Please note that the output that you see may differ slightly, as more tests may be added.
e@ubuntu:~/Desktop/expertiza$ bundle exec rspec spec/helpers/authorization_helper_spec.rb [...] Randomized with seed 45936 [...] 106 examples, 0 failures [...] e@ubuntu:~/Desktop/expertiza$ bundle exec rspec spec/helpers/authorization_helper_spec.rb [...] Randomized with seed 34039 [...] 106 examples, 0 failures [...] e@ubuntu:~/Desktop/expertiza$ bundle exec rspec spec/helpers/authorization_helper_spec.rb [...] Randomized with seed 47123 [...] 106 examples, 0 failures
- The test suite for a single helper method is below. There are many such suites in authorization_helper_spec.rb (link). This example illustrates our general strategy: test missing input, test bad input, test various acceptable forms of input, test scenarios that lead to "true" and to "false" return values.
describe ".current_user_has_id?" do it 'returns false if there is no current user' do [...] it 'returns false if current user exists but an erroneous id is passed in' do [...] it 'returns false if passed in id does not match current user id' do [...] it 'returns true if passed in id matches the current user id' do [...] it 'returns true if passed in id is the string version of current user id' do [...] end
Team Members
- Andrew Miller
- Aurora Tiffany-Davis
- Ginger Balmat
References (Our Work)
- GitHub Project Repository Fork
- GitHub Pull Request
- Video - Demonstration of Authorization - Impersonate User feature
- Video - Demonstration of Authorization - View a Review