CSC/ECE 517 Fall 2023 - E2377. Reimplement impersonating users (functionality within impersonate controller.rb): Difference between revisions
(21 intermediate revisions by 3 users not shown) | |||
Line 9: | Line 9: | ||
== Solution == | == Solution == | ||
We have converted the impersonate users functionality from the MVC pattern in Expertiza to match the Rails API specification for the reimplementation backend repository. We have reimplemented the impersonate_controller.rb file located within expertiza/app/controllers/. The link to the original controller within expertiza can be found at this link: [https://github.com/expertiza/expertiza/blob/main/app/controllers/impersonate_controller.rb Original Controller] | |||
This is not the only file that we have edited. We also needed to edit users.rb file to add some functionality regarding the impersonation criteria. We have used JWT Tokens for authentication. The front end will still be responsible for storing the original user to get the original user back in function. Our code works work to change the current user as and when necessary in the API. We also ensure that user privileges are checked to authorize only the relevant users to impersonate student users. Users that are super admin, TAs for other users or recursively parents of the user they are trying to impersonate can impersonate the user. | |||
We have also ensured that we have the supporting methods in the User class implemented. One such example of a method that we have reimplemented is the “can_impersonate?” method within the User class. | |||
== Design == | == Design == | ||
[[File:UML_diagram_E2377.png|900px|UML Diagram]] | |||
<br> | |||
[[File:Hierarchy_diagram_E2377.png|500px|hierarchy_diagram]] | |||
== Methods Reimplemented == | == Methods Reimplemented == | ||
'''Original''' | |||
Original | |||
<pre>def action_allowed? | <pre>def action_allowed? | ||
# Check for TA privileges first since TA's also have student privileges. | # Check for TA privileges first since TA's also have student privileges. | ||
Line 28: | Line 39: | ||
'''Modification''' : | |||
We have removed the session and stored the user with a JWT token to authenticate. So this method is no longer required. | |||
Original | '''Original''' | ||
<pre>def auto_complete_for_user_name | <pre>def auto_complete_for_user_name | ||
Line 41: | Line 50: | ||
end</pre> | end</pre> | ||
'''Modification''' : | |||
Instead of using session we just access the current user, and since this is a frontend task we decided to not keep it. | |||
Original | '''Original''' | ||
<pre> | <pre> | ||
Line 54: | Line 62: | ||
</pre> | </pre> | ||
'''Modification''' : | |||
We removed the flash error as it will not work in the our new implementation. | |||
Original | '''Original''' | ||
<pre> def generate_session(user) | <pre> def generate_session(user) | ||
AuthController.clear_user_info(session, nil) | AuthController.clear_user_info(session, nil) | ||
Line 67: | Line 75: | ||
'''Modification''' : | |||
This method is now called inside the 'impersonate' method using JWT token | |||
'''Original''' | |||
<pre>def overwrite_session | <pre>def overwrite_session | ||
if params[:impersonate].nil? | if params[:impersonate].nil? | ||
Line 85: | Line 93: | ||
end</pre> | end</pre> | ||
'''Modification''' : | |||
We got rid of this method as we now authenticate the user with a JWT token instead. | |||
Original | '''Original''' | ||
<pre>def check_if_input_is_valid | <pre>def check_if_input_is_valid | ||
if params[:user] && warn_for_special_chars(params[:user][:name], 'Username') | if params[:user] && warn_for_special_chars(params[:user][:name], 'Username') | ||
Line 99: | Line 107: | ||
end</pre> | end</pre> | ||
'''Modification''' : | |||
We have changed the number of parameters for this method. Instead of two, this method now only takes one parameter which is the username of the user we wish to impersonate. We also have removed the flash errors. | |||
'''Modified code''' | |||
<pre> def check_if_input_is_valid | |||
if params[:impersonate].blank? || warn_for_special_chars(params[:impersonate], 'Username') | |||
# render json: { success: false, error: 'Please enter valid user name' }, status: :unprocessable_entity | |||
end | |||
end | |||
def warn_for_special_chars(str, field_name) | |||
puts str | |||
if contains_special_chars? str | |||
render json: { success: false, error: field_name + " must not contain special characters '" + special_chars + "'." }, status: :unprocessable_entity | |||
return true | |||
end | |||
false | |||
end | |||
def contains_special_chars?(str) | |||
special = special_chars | |||
regex = /[#{Regexp.escape(special)}]/ | |||
!str.match(regex).nil? | |||
end | |||
def special_chars | |||
'/\\?<>|&$#' | |||
end | |||
</pre> | |||
'''Modification''' : | |||
This new method checks and warns about special characters used in the input. The special characters it checks for are /\\?<>|&$# | |||
'''Original''' | |||
<pre>def check_if_user_impersonateable | <pre>def check_if_user_impersonateable | ||
if params[:impersonate].nil? | if params[:impersonate].nil? | ||
Line 119: | Line 159: | ||
end | end | ||
end</pre> | end</pre> | ||
'''Modification''' : | |||
We changed the session to a JWT token in this method. | |||
'''Modified code''' | |||
<pre> def check_if_user_impersonatable? | |||
user = User.find_by(name: params[:impersonate] ) | |||
if user | |||
return @current_user.can_impersonate? user | |||
end | |||
false | |||
end | |||
</pre> | |||
'''Original''' | |||
<pre> | <pre> | ||
def impersonate | def impersonate | ||
Line 154: | Line 207: | ||
end</pre> | end</pre> | ||
'''Modification''' : | |||
Modified code using JWT tokens | |||
<pre> | |||
def impersonate | |||
if check_if_user_impersonatable? | |||
user = User.find_by(name: params[:impersonate]) | |||
if user | |||
impersonate_payload = { id: user.id, name: user.name, full_name: user.full_name, role: user.role.name, | |||
institution_id: user.institution.id, impersonate: true, original_user: @current_user } | |||
impersonate_token = JsonWebToken.encode(impersonate_payload, 24.hours.from_now) | |||
render json: { success: true, token: impersonate_token, message: "Successfully impersonated #{user.name}" } | |||
else | |||
render json: { success: false, error: 'User not found' }, status: :not_found | |||
end | |||
else | |||
render json: { success: false, error: "You don't have permission to impersonate this user" }, status: :forbidden | |||
end | |||
end | |||
</pre> | |||
Original | '''Modification''' : | ||
In order to check if the user can be impersonated, we delegate it to the user class. The user class now handles this part of checking | |||
'''Modified code in user.rb''' | |||
<pre> | |||
def can_impersonate?(user) | |||
return true if role.super_administrator? | |||
return true if teaching_assistant_for?(user) | |||
return true if recursively_parent_of(user) | |||
false | |||
end | |||
def teaching_assistant_for?(student) | |||
return false unless ta? | |||
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) | |||
ta.managed_users.each { |user| | |||
if user.id == student.id | |||
return true | |||
end | |||
} | |||
false | |||
end | |||
def recursively_parent_of(user) | |||
p = user.parent | |||
return false if p.nil? | |||
return true if p == self | |||
return false if p.role.super_administrator? | |||
recursively_parent_of(p) | |||
end | |||
end | |||
</pre> | |||
'''Original''' | |||
<pre>def real_user(name) | <pre>def real_user(name) | ||
Line 168: | Line 282: | ||
end</pre> | end</pre> | ||
'''Modification''' : | |||
'''IMPORTANT''' | |||
We believe that this project is a mix of frontend and backend. So the assumption we have made is that when a user wants to impersonate any other user, the JWT token for the current user is stored in the frontend. And when the 'session' for impersonation is over, the user is assigned their previous JWT token retrieved from the frontend. | |||
== Test Plan == | == Test Plan == | ||
For the testing of this project, we first tested all functionality using the Postman API. | |||
We test by creating tokens for each user logged in and check if the logged in user (current user) can impersonate any other user or not. | |||
We also check if adding special characters in the URL lets us impersonate other users or throws an appropriate error. | |||
The working of the Postman testing is included in the vidoe link below. | |||
Later we moved on to testing our program using the RSwag tool. This allowed us to test and explore operations using a UI and directly from the rspec integration tests. <br> | |||
The video links for these testing methods - https://drive.google.com/file/d/19BTXHedw4iUgbIQf8c5cWdKpnnNUVm6k/view?usp=sharing | |||
==Team== | ==Team== | ||
Line 188: | Line 308: | ||
==Pull Request== | ==Pull Request== | ||
Changes for this project are under Expertiza Pull Request | Changes for this project are under Expertiza Pull Request | ||
Link - https://github.com/expertiza/reimplementation-back-end/pull/68 |
Latest revision as of 02:49, 5 December 2023
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.
Problem Statement
The current expertiza platform allows authorized users to impersonate others, utilizing session-based authentication. The challenge is to reimplement this feature in the new expertiza backend (https://github.com/expertiza/reimplementation-back-end), which employs JWT token authentication.
Key challenges include adapting the existing session-based logic to work seamlessly with JWT tokens and transitioning from a Model-View-Controller (MVC) architecture to an API-only setup, requiring responses in JSON format. The goal is to ensure the smooth integration of the impersonation feature into the new backend while addressing authentication changes and architectural shifts.
Solution
We have converted the impersonate users functionality from the MVC pattern in Expertiza to match the Rails API specification for the reimplementation backend repository. We have reimplemented the impersonate_controller.rb file located within expertiza/app/controllers/. The link to the original controller within expertiza can be found at this link: Original Controller
This is not the only file that we have edited. We also needed to edit users.rb file to add some functionality regarding the impersonation criteria. We have used JWT Tokens for authentication. The front end will still be responsible for storing the original user to get the original user back in function. Our code works work to change the current user as and when necessary in the API. We also ensure that user privileges are checked to authorize only the relevant users to impersonate student users. Users that are super admin, TAs for other users or recursively parents of the user they are trying to impersonate can impersonate the user. We have also ensured that we have the supporting methods in the User class implemented. One such example of a method that we have reimplemented is the “can_impersonate?” method within the User class.
Design
Methods Reimplemented
Original
def action_allowed? # Check for TA privileges first since TA's also have student privileges. if ['Student'].include? current_role_name !session[:super_user].nil? else ['Super-Administrator', 'Administrator', 'Instructor', 'Teaching Assistant'].include? current_role_name end end
Modification :
We have removed the session and stored the user with a JWT token to authenticate. So this method is no longer required.
Original
def auto_complete_for_user_name @users = session[:user].get_available_users(params[:user][:name]) render inline: "<%= auto_complete_result @users, 'name' %>", layout: false end
Modification : Instead of using session we just access the current user, and since this is a frontend task we decided to not keep it.
Original
def start flash[:error] = "This page doesn't take any query string." unless request.GET.empty? end
Modification : We removed the flash error as it will not work in the our new implementation.
Original
def generate_session(user) AuthController.clear_user_info(session, nil) session[:original_user] = @original_user session[:impersonate] = true session[:user] = user end
Modification :
This method is now called inside the 'impersonate' method using JWT token
Original
def overwrite_session if params[:impersonate].nil? user = real_user(params[:user][:name]) session[:super_user] = session[:user] if session[:super_user].nil? generate_session(user) elsif !params[:impersonate][:name].empty? user = real_user(params[:impersonate][:name]) generate_session(user) else session[:user] = session[:super_user] session[:super_user] = nil end end
Modification : We got rid of this method as we now authenticate the user with a JWT token instead.
Original
def check_if_input_is_valid if params[:user] && warn_for_special_chars(params[:user][:name], 'Username') flash[:error] = 'Please enter valid user name' redirect_back fallback_location: root_path elsif params[:impersonate] && warn_for_special_chars(params[:impersonate][:name], 'Username') flash[:error] = 'Please enter valid user name' redirect_back fallback_location: root_path end end
Modification : We have changed the number of parameters for this method. Instead of two, this method now only takes one parameter which is the username of the user we wish to impersonate. We also have removed the flash errors.
Modified code
def check_if_input_is_valid if params[:impersonate].blank? || warn_for_special_chars(params[:impersonate], 'Username') # render json: { success: false, error: 'Please enter valid user name' }, status: :unprocessable_entity end end def warn_for_special_chars(str, field_name) puts str if contains_special_chars? str render json: { success: false, error: field_name + " must not contain special characters '" + special_chars + "'." }, status: :unprocessable_entity return true end false end def contains_special_chars?(str) special = special_chars regex = /[#{Regexp.escape(special)}]/ !str.match(regex).nil? end def special_chars '/\\?<>|&$#' end
Modification : This new method checks and warns about special characters used in the input. The special characters it checks for are /\\?<>|&$#
Original
def check_if_user_impersonateable if params[:impersonate].nil? user = real_user(params[:user][:name]) unless @original_user.can_impersonate? user @message = "You cannot impersonate '#{params[:user][:name]}'." temp AuthController.clear_user_info(session, nil) else overwrite_session end else unless params[:impersonate][:name].empty? overwrite_session end end end
Modification : We changed the session to a JWT token in this method.
Modified code
def check_if_user_impersonatable? user = User.find_by(name: params[:impersonate] ) if user return @current_user.can_impersonate? user end false end
Original
def impersonate begin @original_user = session[:super_user] || session[:user] if params[:impersonate].nil? @message = "You cannot impersonate '#{params[:user][:name]}'." @message = 'User name cannot be empty' if params[:user][:name].empty? user = real_user(params[:user][:name]) check_if_user_impersonateable if user elsif !params[:impersonate][:name].empty? # Impersonate a new account @message = "You cannot impersonate '#{params[:impersonate][:name]}'." user = real_user(params[:impersonate][:name]) check_if_user_impersonateable if user # Revert to original account when currently in the impersonated session elsif !session[:super_user].nil? AuthController.clear_user_info(session, nil) session[:user] = session[:super_user] user = session[:user] session[:super_user] = nil end # Navigate to user's home location as the default landing page after impersonating or reverting AuthController.set_current_role(user.role_id, session) redirect_to action: AuthHelper.get_home_action(session[:user]), controller: AuthHelper.get_home_controller(session[:user]) rescue StandardError flash[:error] = @message redirect_back fallback_location: root_path end end
Modification : Modified code using JWT tokens
def impersonate if check_if_user_impersonatable? user = User.find_by(name: params[:impersonate]) if user impersonate_payload = { id: user.id, name: user.name, full_name: user.full_name, role: user.role.name, institution_id: user.institution.id, impersonate: true, original_user: @current_user } impersonate_token = JsonWebToken.encode(impersonate_payload, 24.hours.from_now) render json: { success: true, token: impersonate_token, message: "Successfully impersonated #{user.name}" } else render json: { success: false, error: 'User not found' }, status: :not_found end else render json: { success: false, error: "You don't have permission to impersonate this user" }, status: :forbidden end end
Modification : In order to check if the user can be impersonated, we delegate it to the user class. The user class now handles this part of checking
Modified code in user.rb
def can_impersonate?(user) return true if role.super_administrator? return true if teaching_assistant_for?(user) return true if recursively_parent_of(user) false end def teaching_assistant_for?(student) return false unless ta? 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) ta.managed_users.each { |user| if user.id == student.id return true end } false end def recursively_parent_of(user) p = user.parent return false if p.nil? return true if p == self return false if p.role.super_administrator? recursively_parent_of(p) end end
Original
def real_user(name) if User.anonymized_view?(session[:ip]) user = User.real_user_from_anonymized_name(name) else user = User.find_by(name: name) end return user end
Modification : IMPORTANT We believe that this project is a mix of frontend and backend. So the assumption we have made is that when a user wants to impersonate any other user, the JWT token for the current user is stored in the frontend. And when the 'session' for impersonation is over, the user is assigned their previous JWT token retrieved from the frontend.
Test Plan
For the testing of this project, we first tested all functionality using the Postman API.
We test by creating tokens for each user logged in and check if the logged in user (current user) can impersonate any other user or not. We also check if adding special characters in the URL lets us impersonate other users or throws an appropriate error.
The working of the Postman testing is included in the vidoe link below.
Later we moved on to testing our program using the RSwag tool. This allowed us to test and explore operations using a UI and directly from the rspec integration tests.
The video links for these testing methods - https://drive.google.com/file/d/19BTXHedw4iUgbIQf8c5cWdKpnnNUVm6k/view?usp=sharing
Team
Mentor
Renji Joseph Sabu <rsabu@ncsu.edu>
Students
Manoj Ayyappan <mayyapp@ncsu.edu>
Pradeep Patil <papatil@ncsu.edu>
Maya Patel <mdpatel2@ncsu.edu>
Pull Request
Changes for this project are under Expertiza Pull Request
Link - https://github.com/expertiza/reimplementation-back-end/pull/68