CSC/ECE 517 Spring 2024 - E2421. Reimplement impersonating users (within impersonate controller.rb) - Final Project
Expertiza
Expertiza is a Ruby on Rails based open source project. Instructors have the ability to add new projects, assignments, etc., as well as edit existing ones. Later on, they can view student submissions and grade them. Students can also use Expertiza to organize into teams to work on different projects and assignments and submit their work. They can also review other students' submissions.
Project Overview
The goal of this project is to update the backend code for the user impersonation feature in the new version of Expertiza. The existing implementation relies on sessions, which is incompatible with the new implementation that uses JWT (JSON Web Tokens) for authentication and JSON responses. The primary challenge is to migrate the impersonation logic from session-based management to JWT-based authentication while preserving the same functionality. This reimplementation requires planning for the communication between the backend and frontend, which may necessitate modifications to existing files or the creation of new files beyond the impersonate_controller.rb file.
Design Pattern
Facade Design Pattern
The Api::V1::ImpersonateController acts as a facade by providing a simplified interface to interact with more complex subsystems (such as user authentication, authorization, and user management) within the application. It encapsulates the logic for impersonating users and fetching user lists behind a single interface.
Strategy Design Pattern
The is_user_impersonateable? method implements a strategy for determining whether the current user has permission to impersonate another user. It checks if the impersonated user exists and if the current user has the necessary permissions to impersonate them. This method encapsulates a specific algorithm for evaluating impersonation permissions, which can be changed or extended independently of the controller logic. Overall, these design pattern principles contribute to a modular and maintainable design by promoting encapsulation, separation of concerns, and flexibility in the implementation of complex behaviors.
Implementation
UML Diagram
The following UML diagram shows the association between models we will be working on
Hierarchy Diagram
Visualization of the hierarchical structure
Request
- GET: {BASE_URL}/api/v1/impersonate/:username
- Response:
"message": "Successfully Fetched User List!", "userList": [ { "id": 4, "name": "Mihir", "full_name": "Mihir BHanderi", "email": "mbhande@example.com", "email_on_review": false, "email_on_submission": false, "email_on_review_of_review": false, "role": { "id": 4, "name": "Teaching Assistant" }, "institution": { "id": 1, "name": "North Carolina State University" }, "parent": { "id": null, "name": null } } ], "success": true
- POST: {BASE_URL}/api/v1/impersonate
- Payload: impersonate_id - Response:
"message": "Successfully Impersonated Ketul!", "token": "eyJhbGciOiJSUzI1NiJ9.eyJpZCI6MywibmFtZSI6IktldHVsIiwiZnVsbF9uYW1lIjoiS2V0dWwgQ2hheXlhIiwicm9sZSI6Ikluc3RydWN0b3IiLCJpbnN0aXR1dGlvbl9pZCI6MSwiaW1wZXJzb25hdGVkIjp0cnVlLCJvcmlnaW5hbF91c2VyIjoiIzxVc2VyOjB4MDAwMDdmMzVmYTc5YjVmOD4iLCJleHAiOjE3MTEzMjc0MTV9.U9wDOT618UCkf25MnCiK8W3ybeZv5BSQDNTEPOMUDABAvDd0HWSj3kIGccITHaoVsIykZFsyUDY3rL_M32zmfEXvxZuEleWSqUZGxbjQRIFP1bR_Q5sPESoBdlxVJ4QG8sUGQtuhOzMyH3z4R4ruhz1JpsQlalVZQHCbdtJOI9B4WKhNJ98Dls1fefnzYwLMnTr6e3ttbGGGK5Bm8zSpPvIWmCVNoueKHNptFcNejbU4Mt9RHWHLsTwdAtuywNLCu7li7RRNXo00D5JOUMxL7eB5AiQRpxah8BF7b0lM_Xh7bB56WvD5JTjoNZu3c3AK_EJksGXiFxwlPzNRc8Q", "success": true
Impersonate Controller
The ImpersonateController facilitates user impersonation functionality. It includes methods to fetch a list of users available for impersonation based on a provided username parameter and to impersonate a selected user by generating a new JWT token with the necessary user information. The controller ensures that impersonation requests are handled securely, validating permissions before allowing impersonation to occur.
- impersonate_controller.rb file
class Api::V1::ImpersonateController < ApplicationController # Fetches users to impersonate whose name match the passed parameter def get_users_list users = current_user.get_available_users(params[:user_name]) render json: { message: "Successfully Fetched User List!", userList:users, success:true }, status: :ok end def is_user_impersonateable? impersonate_user = User.find_by(id: params[:impersonate_id]) if impersonate_user return current_user.can_impersonate? impersonate_user end false end # Impersonates a new user and returns new jwt token def impersonate unless params[:impersonate_id].present? render json: { error: "impersonate_id is required", success:false }, status: :unprocessable_entity return end if is_user_impersonateable? impersonate_user = User.find_by(id: params[:impersonate_id]) payload = { id: impersonate_user.id, name: impersonate_user.name, full_name: impersonate_user.full_name, role: impersonate_user.role.name, institution_id: impersonate_user.institution.id, impersonated:true, original_user: current_user } impersonate_user_token = JsonWebToken.encode(payload, 24.hours.from_now) render json: { message: "Successfully Impersonated #{impersonate_user.name}!", token:impersonate_user_token, success:true }, status: :ok else render json: { error: "You do not have permission to impersonate this user", success:false }, status: :forbidden end end end
User
These methods extend the functionality of the User model. The get_available_users method retrieves users whose full names match a provided parameter. can_impersonate? determines whether the user has the authority to impersonate another user based on their role hierarchy. teaching_assistant_for? checks if the user is a teaching assistant for a given student, and teaching_assistant? determines if the user is a teaching assistant based on their role. Lastly, recursively_parent_of recursively checks for parent-child relationships between user roles.
- user.rb file
has_many :assignments, through: :participants # Fetches available users whose full names match the provided name prefix (case-insensitive). # Returns a limited list of users (up to 10) who have roles similar or subordinate to the current user's role. def get_available_users(name) lesser_roles = role.subordinate_roles_and_self all_users = User.where('full_name LIKE ?', "%#{name}%").limit(20) visible_users = all_users.select { |user| lesser_roles.include? user.role } visible_users[0, 10] # the first 10 end # Check if the user can impersonate another user def can_impersonate?(user) return true if role.super_administrator? return true if instructor_for?(user) # Skip below check if user's role is "Instructor" return false if instructor? return true if teaching_assistant_for?(user) # Skip recursively_parent_of check if user's role is "Teaching Assistant" return false if teaching_assistant? return true if recursively_parent_of(user.role) false end # Check if the current user is an instructor and has any relationship with the given user (student or TA) def instructor_for?(user) return false unless instructor? return true if instructor_for_student?(user) return true if instructor_for_ta?(user) end # Helper method to check if there are any courses where a student is enrolled in assignments def courses_with_student_participation(courses, student) courses.any? do |course| course.assignments.any? do |assignment| assignment.participants.map(&:user_id).include?(student.id) end end end # Check if the instructor has any relationship with the given student def instructor_for_student?(student) return false unless student.role.name == 'Student' # Ensure the role is 'Student' instructor = Instructor.find(id) # Check if the instructor has any courses where the student is enrolled in an assignment return courses_with_student_participation(Instructor.list_all(Course, instructor),student) end # Check if the instructor has common courses with the given teaching assistant def instructor_for_ta?(ta) return false unless ta.role.name == 'Teaching Assistant' # Ensure the role is 'Teaching Assistant' instructor = Instructor.find(id) # Get all courses taught by the instructor instructor_courses = Instructor.list_all(Course, instructor) # Get all courses associated with the TA ta_courses = TaMapping.get_courses(ta) # Convert lists to sets for efficient intersection instructor_course_set = instructor_courses.to_set ta_course_set = ta_courses.to_set # Check for common courses using set intersection has_common_course = !(instructor_course_set & ta_course_set).empty? return has_common_course end # Check if the user is a teaching assistant for the student's course def teaching_assistant_for?(student) return false unless teaching_assistant? return false unless student.role.name == 'Student' # We have to use the Ta object instead of User object # because single table inheritance is not currently functioning ta = Ta.find(id) # Check if the TA has any courses where the student is enrolled in an assignment return courses_with_student_participation(TaMapping.get_courses(ta),student) false end # Check if the user is a teaching assistant def teaching_assistant? true if role.ta? end # Recursively check if parent child relationship exists def recursively_parent_of(user_role) p = user_role.parent return false if p.nil? return true if p == self.role return false if p.super_administrator? recursively_parent_of(p) end
Assignment
The Assignment model represents a task or project that is associated with a course. It is linked to a specific course and involves multiple participants (users) who are involved in or responsible for completing the assignment. Each assignment may have various users associated with it, depending on their role or participation within the course.
- assignment.rb file
belongs_to :course has_many :participants, dependent: :destroy has_many :users, through: :participants, inverse_of: :assignment
Course
Fixed the assignment mapping
- course.rb file
has_many :assignments, dependent: :destroy
Instructor
Created a method to get all the courses assigned to the specific instructor
- instructor.rb file
def self.list_all(object_type, user_id) object_type.where('instructor_id = ? AND private = 0', user_id) end
TA
Refactored the code to handle a error if TA is not assigned to any course.
- ta.rb file
class Ta < User has_many :ta_mappings, dependent: :destroy
- ta_mapping.rb file
#Returns course ids of the TA def self.get_course_ids(user_id) TaMapping.find_by(ta_id: user_id).course_id ta_mapping = TaMapping.find_by(user_id: user_id) ta_mapping&.course_id end #Returns courses of the TA def self.get_courses(user_id) Course.where('id = ?', get_course_ids(user_id)) course_ids = get_course_ids(user_id) return Course.none unless course_ids # Return Course.none if course_ids is nil Course.where(id: course_ids) end end
Files added / modified
- reimplementation-back-end/app/models/user.rb
- reimplementation-back-end/app/controllers/api/v1/impersonate_controller.rb
- reimplementation-back-end/app/models/course.rb
- reimplementation-back-end/app/models/instructor.rb
- reimplementation-back-end/app/models/ta.rb
- reimplementation-back-end/app/models/ta_mapping.rb
- reimplementation-back-end/config/routes.rb
List of Users in Database
ID | Password | Role | |
---|---|---|---|
1 | admin2@example.com | password123 | Super-Administrator |
2 | jay@example.com | password123 | Administration |
3 | k2l@example.com | password123 | Instructor |
4 | mbhande@example.com | password123 | TA |
7 | dpatesl@example.com | password123 | Student |
Testing on Postman
Postman was used to manually test the additional method in impersonate_controller.rb, as well as the actions and routes of the corresponding controllers. Before testing any of these methods with Postman, submit a request to /login using the user_name and password fields, which will send an authentication token. This token must be added to Postman's 'Authorization' tab as a 'Bearer token' before any further requests can be made. Postman collection link
- First of all, fork the expertiza workspace in order to work upon it
- Fetch User List which can be impersonated (For eg: If Instructor fetches list he can see matched TAs and Students only)
- Login with provided credentials and copy the token
- In the Expertiza/Authorization paste the token and save
- Put the id for the user to be impersonated
- Success message if the user is impersonable
- Failure message if the user is not impersonable (Here instructor is trying to impersonate admin)
Swagger UI Documentation
Here is the video of successfully running the tests [1]
Here is the demo video of working of the project [2]
Team
Mentor
- Chetana Chunduru <cchetan2@ncsu.edu>
Students
- Devansh Shah <dshah8@ncsu.edu>
- Jay Patel <jhpatel9@ncsu.edu>
- Mihir Bhanderi <mbhande2@ncsu.edu>