concord-consortium/rigse

View on GitHub
rails/app/controllers/api/api_controller.rb

Summary

Maintainability
C
1 day
Test Coverage
class API::APIController < ApplicationController

  rescue_from ActionController::ParameterMissing, with: :parameter_missing
  rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found

  def show
    return error("Show not configured for this resource")
  end

  def create
    return error("create not configured for this resource")
  end

  def update
    return error("update not configured for this resource")
  end

  def index
    return error("index not configured for this resource")
  end

  def destroy
    return error("destroy not configured for this resource")
  end

  def check_for_auth_token(params)
    header = request.headers["Authorization"]
    if header && header =~ /^Bearer (.*)$/i
      token = $1
      grant = AccessGrant.find_by_access_token(token)

      if grant
        if grant.access_token_expires_at >= Time.now
          return [grant.user, {:learner => grant.learner, :teacher => grant.teacher}]
        else
          raise StandardError, 'AccessGrant has expired'
        end

      # peer to peer authentication based on app_secret is available if the learner id is passed
      elsif params[:learner_id_or_key]
        begin
          # find_by_id_or_key uses find! so we need to catch the exception
          # NOTE: we should probably rename it to find_by_id_or_key! so that callers know it
          # generates an exception unlike normal find_by_x methods
          learner = Portal::Learner.find_by_id_or_key(params[:learner_id_or_key])
        rescue
          raise StandardError, "Cannot find learner with id or key of '#{params[:learner_id_or_key]}'"
        end
        peer = Client.find_by_app_secret(token)
        if peer
          return [learner.student.user, {:learner => learner, :teacher => nil}]
        else
          raise StandardError, "Cannot find requested peer token" # don't leak token value in error
        end

      # peer to peer authentication based on app_secret is available if the user id is passed
      elsif params[:user_id]
        user = User.find_by_id(params[:user_id])
        if user
          peer = Client.find_by_app_secret(token)
          if peer
            return [user, {:learner => nil, :teacher => nil}]
          else
            raise StandardError, "Cannot find requested peer token" # don't leak token value in error
          end
        else
          raise StandardError, "Cannot find user with id of '#{params[:user_id]}'"
        end
      else
        # NOTE: token value was removed from error so we don't leak peer tokens
        raise StandardError, "Cannot find AccessGrant for requested token"
      end

    elsif header && header =~ /^Bearer\/JWT (.*)$/i
      portal_token = $1
      # if invalid this will raise a SignedJWT::Error which is a subclass of StandardError that the caller should be listening for
      # the expiration is checked within the JWT.decode function
      decoded_token = SignedJWT::decode_portal_token(portal_token)
      data = decoded_token[:data]

      user = User.find_by_id(data["uid"])
      if user
        role = {
          :learner => data["user_type"] == "learner" ? Portal::Learner.find_by_id(data["learner_id"]) : nil,
          :teacher => data["user_type"] == "teacher" ? Portal::Teacher.find_by_id(data["teacher_id"]) : nil
        }
        return [user, role]
      else
        raise StandardError, 'User in token not found'
      end

    elsif current_user
      return [current_user, nil]
    else
      raise StandardError, 'You must be logged in to use this endpoint'
    end
  end

  protected

  # NOTE: this approach requires you to return from the
  # method to prevent a double render problem. An easy way to do this:
  #  return error(...)
  def error(message, status = 400, details = nil)
    error_body = {
      :success => false,
      :response_type => "ERROR",
      :message => message,
    }
    error_body[:details] = details if details
    render :json => error_body, :status => status
  end

  def pundit_user_not_authorized(exception)
    render status: 403, json: {
      success: false,
      message: 'Not authorized'
    }
  end

  def parameter_missing(exception)
    render status: 400, json: {
      success: false,
      message: exception.message
    }
  end

  def record_not_found(exception)
    render status: 404, json: {
      success: false,
      message: exception.message
    }
  end

  def auth_not_anonymous(params)
    begin
      user, role = check_for_auth_token(params)
    rescue StandardError => e
      return {error: e.message}
    end

    if user.anonymous?
      return {error: 'You must be logged in to use this endpoint'}
    end

    return {user: user, role: role}
  end

  def auth_teacher(params)
    auth = auth_not_anonymous(params)
    return auth if auth[:error]
    user = auth[:user]

    if !user.portal_teacher
      auth[:error] = 'You must be logged in as a teacher to use this endpoint'
    end

    return auth
  end
end