Author: Alastair Dawson 

Overview

Goal

Separate the concern of authorization from the protected service.

Background

Endpoints are currently protected at a high level by authentication from an OIDC provider. Once a user logs in, services they have access to are included in the navigation on the front-end. Requests to those service endpoints are then rechecked against user attributes from the back-end service classes. 

The logic for navigation filtration lives in the User object and gets called by the UserSerializer. Some of that logic is shared by the back-end services, although many supplement it with additional checks on the user and other submitted parameters. This has lead to a bit of a fractured authorization story; business rules for authorization are not currently self documenting or easily discoverable. 

Although we have auth checks in the services themselves it's more secure, and efficient, to return an error before a controller action if called. This will allow services to more closely follow the single responsibility principle, reduce service code, and make test cases more succinct.

High level design

  • Extract the authorization methods from the User object and service classes into a policy per service. 

  • Add before_filters in service controllers to check the related policy file.

  • Update application controller to raise 403 Forbidden errors when the auth framework rejects a user.

  • Update UserSerializer to use the policy classes rather than the methods on User.

Specifics

Detailed Design

The Pundit gem provides a set of helpers that hook into Rails using regular Ruby classes. The standard usage of the Pundit is to depend on ActiveRecord based models to check authorization. As most vets-api resources are accessed through services we'll need to use the headless policies feature. Below is an example of updating the EVSS letters service to use pundit.

The policy file for all EVSS routes check the same policy method can?:

EVSSPolicy = Struct.new(:user, :evss) do  
  def can?  
    user.edipi.present? && user.ssn.present? && user.participant_id.present?  
  end  
end
RUBY

If some endpoints require different authorization, additional methods can be added:

EVSSPolicy = Struct.new(:user, :evss) do  
  def can?  
    user.edipi.present? && user.ssn.present? && user.participant_id.present?  
  end  

  def can_update_ssn?  
    user.ssn.present?  
  end
end
RUBY

Shared authorization can be defined in base controllers:

module V0
  class EVSSController < ApplicationController
  
  private

  def authorize_user  
    authorize :evss, :can?  
  end
end
RUBY

A child controller can use a before_action to add authorization for all endpoints:

module V0  
  class LettersController < EVSSController  
    before_action :authorize_user
RUBY

If a controller’s actions don’t need authorization we can use before_action filters to skip them:

before_action :authorize_user, except:[:index]
RUBY

If an action has different auth rules there are two options:

  1. Add another before_action filtered for that action

    • before_action :authorize_user, except:[:update_ssn]
      before_action :authorize_user_ssn, only:[:update_ssn]
      RUBY
  2. Call the pundit authorize method from within the action’s method

    • def update_ssn
        authorize :evss, :can_update_ssn?
      end
      RUBY

ApplicationController will need to be updated to catch Pundit::NotAuthorizedError errors:

va_exception =
  case exception
  when Pundit::NotAuthorizedError
    Common::Exceptions::Forbidden.new('User does not have access to the requested resource')
  when ActionController::ParameterMissing
    Common::Exceptions::ParameterMissing.new(exception.param)
RUBY

Alternatives and Future work

An alternative discussed at the Ad Hoc/va.gov day is to break authorization out into a micro service. As vets-api currently stands this may be premature optimization. If vets-api's services get broken out/extended with a Zapier like directory of VA APIs this may make more sense. Authorization could be part of a smart reverse proxy. The proxy would check that a user has access to the request endpoint. If the user is approved, the request is then forwarded on to the appropriate app/micro service.