CSC/ECE 517 Fall 2023 - E2377. Reimplement impersonating users (functionality within impersonate controller.rb): Difference between revisions

From Expertiza_Wiki
Jump to navigation Jump to search
Line 26: Line 26:




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 40: Line 40:




Update
'''Modification''' :
We have removed the session and stored the user with a JWT token to authenticate. So this method is no longer required.
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 51: Line 51:
   end</pre>
   end</pre>


Update
'''Modification''' :
 
Instead of using session we just access the current user, and since this is a frontend task we decided to not keep it.  
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 64: Line 63:
</pre>
</pre>


Updated
'''Modification''' :
We removed the flash error as it will not work in the our new implementation.
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 77: Line 76:




Updated
'''Modification''' :
This method is now called inside the 'impersonate' method using JWT token
This method is now called inside the 'impersonate' method using JWT token


Original
'''Original'''
 
   <pre>def overwrite_session
   <pre>def overwrite_session
     if params[:impersonate].nil?
     if params[:impersonate].nil?
Line 96: Line 94:
   end</pre>
   end</pre>


Updated
'''Modification''' :
We got rid of this method as we now authenticate the user with a JWT token instead.  
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 110: Line 108:
   end</pre>
   end</pre>


Updated:
'''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.
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.


  Original
'''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 130: Line 160:
     end
     end
   end</pre>
   end</pre>
Updated:


--we will change the session to a JWT token in this method
'''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  
'''Original'''


   <pre>
   <pre>
Line 169: Line 208:
   end</pre>
   end</pre>


Updated:
'''Modification''' :
-- we will change the session to a JWT token and flash message from this method.
Modified code using JWT tokens


Original:
<pre>
def impersonate
      if check_if_user_impersonatable?
        user = User.find_by(name: params[:impersonate])


  <pre>def real_user(name)
        if user
    if User.anonymized_view?(session[:ip])
          impersonate_payload = { id: user.id, name: user.name, full_name: user.full_name, role: user.role.name,
      user = User.real_user_from_anonymized_name(name)
                          institution_id: user.institution.id, impersonate: true, original_user: @current_user }
    else
          impersonate_token = JsonWebToken.encode(impersonate_payload, 24.hours.from_now)
       user = User.find_by(name: name)
 
          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
     end
    return user
</pre>
  end</pre>
 
'''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


Updated:
'''Modified code in user.rb'''
-- we will implement a JWT token instead of using a session here


Original <br>
<pre>
<pre>
def can_impersonate?(user)
  def can_impersonate?(user)
     return true if role.super_admin?
     return true if role.super_administrator?
     return true if teaching_assistant_for?(user)
     return true if teaching_assistant_for?(user)
     return true if recursively_parent_of(user)
     return true if recursively_parent_of(user)


     false
     false
   
   end
   end
</pre>
  def teaching_assistant_for?(student)
    return false unless ta?
    return false unless student.role.name == 'Student'


-- We will reimplement these methods to work with our JWT based authentication system
    # 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


Original
  def recursively_parent_of(user)
<pre>
def recursively_parent_of(user)
     p = user.parent
     p = user.parent
     return false if p.nil?
     return false if p.nil?
     return true if p == self
     return true if p == self
     return false if p.role.super_admin?
     return false if p.role.super_administrator?


     recursively_parent_of(p)
     recursively_parent_of(p)
   end
   end
end
</pre>
</pre>


-- We will reimplement these methods to work with our JWT based authentication system
'''Original'''


Original
  <pre>def real_user(name)
<pre>
     if User.anonymized_view?(session[:ip])
def teaching_assistant_for?(student)
      user = User.real_user_from_anonymized_name(name)
     return false unless teaching_assistant?
     else
    return false unless student.role.name == 'Student'
       user = User.find_by(name: name)
 
    # We have to use the Ta object instead of User object
    # because single table inheritance is not currently functioning
    ta = Ta.find(id)
     return true if ta.courses_assisted_with.any? do |c|
       c.assignments.map(&:participants).flatten.map(&:user_id).include? student.id
     end
     end
   end
    return user
</pre>
   end</pre>


-- We will reimplement these methods to work with our JWT based authentication system
'''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 ==

Revision as of 16:34, 4 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 plan to convert the impersonate users functionality from the MVC pattern in Expertiza to match the Rails API specification for the reimplementation backend repository. To do this we will reimplement the impersonate_controller.rb file located within expertiza/app/controllers/. The link to the original controller within expertizsa can be found at this link: Original Controller This is not the only file that we will be editing. We will be using 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 will work to change the current user as and when necessary in the API. Will 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 will also ensure that we have the supporting methods in the User class are implemented. One such example of a method that we may need to reimplement is “can_impersonate?” within the User class.


Design

UML Diagram

hierarchy_diagram

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 will first test all functionality using the Postman API. If everything works as expected, we will then start testing our program using the RSwag tool. This will allow us to test and explore operations using a UI and directly from the rspec integration tests.
The video links for these testing methods will be posted here when we complete it.

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