In this section you will configure the application to implement Single Sign On (SSO) for security: the worklist displays protected health information (PHI) so it should ensure only authorized personnel can view this data. SSO integration is included in the
harbinger-rails-extensions gem (documented here, but you can also use the SDK methods directly.
If you have been following the tutorial or are using the vanilla application as instructed, you can skip this setup, as it has already been done.
If not you'll need to add the harbinger-rails-extensions gem to your
Gemfile (updating the version to be current):
gem 'harbinger-rails-extensions', '~> 0.0.20', source: 'http://gem.analytical.info:9292'
Bridge SSO integrates with an existing directory service or SSO federation, so the platform does not have its own usernames or passwords. The SSO integration via the SDK can be used to provide authentication, by linking a users directory login with an Employee record in the data model.
Authentication only validates that a user is who they claim to be, it does not validate if they have permission to use a particular resource. The authorization in Bridge authorization is determined by clinical role associated with the employee record. Retrieve an employee record in the console:
me = Java::HarbingerSdkData::Employee.withUserName("myusername") # Test if you are in the 'it-staff' clinical role me.authorizedAs("it-staff")
Best Practice - Static query methods (such as
createQuery) take an optional entity manager as the last argument. While debugging or testing on the console you don't need pass an entity manager, but you should always pass in an entity manager when using these methods in an application.
Employee records have methods for checking if a user is in given list of clinical roles:
- authorizedAs (role) - This takes a clinical role as a string and checks if that employee is in that clinical role.
- authorizedForAny (ArrayOfRoles) - This takes an Array of roles (strings) and checks if the user is a member of any. If they are a member in any of the roles in the array, it returns true.
- authorizedForAll (ArrayOfRoles) - This takes an Array of roles (strings) and checks if the user is a member of all. If they are not a member in one or more roles, it returns false.
A detailed list of the current clinical roles and their intended use is available in the
Service Tools application under the navbar
Clinical Roles (under the heading Reference)
Some clinical roles are meta- or modifier roles. An example is the
supervisor role. The combination of
radiologist would be a section chief type role. To check that these roles exist together you can use the
authorizedForAll method or you can group them together in the
authorizedForAny method. Here is an example of doing both.
# Check for a supervisor radiologist or a it-staff me.authorizedForAll(["radiologist","supervisor"]) or me.authorizedAs("it-staff") # Do the same thing in one function me.authorizedForAny([["radiologist","supervisor"],"it-staff"])
To implement authorization in an application without
harbinger-rails-extensions, you'd need to create equivalent methods using the functions provided in
Java::HarbingerSdk::SSO to get the cookie name, retrieve the data using the application framework, then check the validity of the token (
SSO.validToken). All of these functions, redirection methods, and the username in set
session[:username] are implemented in
harbinger-rails-extensions for convenience.
The tutorial application implements a
before_filter to create an entity manager for controllers. A
before_filter can also be used to redirect unauthenticated and unauthorized requests with the method
authenticate_and_authorized (provided by
harbinger-rails-extensions to controllers). Edit
app/controllers/application_controller.rb to add a new method:
def general_authentication authenticate_and_authorize(["ai-staff","it-staff","radiologist"]) end
Add that method in a before_filter to the
class WorklistController < ApplicationController before_filter :general_authentication #... end
After these changes, navigation to
/worklist will prompt an SSO login. If you successfully log in, but you are not in the list of authorized clinical roles, you will be redirected to a page that doesn't currently exist in the application. The redirect is to the index method of the
UnauthorizedController. Create that controller and a route to the index method, starting in
get 'unauthorized' => "unauthorized#index"
Next, create the controller (
class UnauthorizedController < ApplicationController def index end end
Finally, create the view by making the
app/views/unauthorized directory and creating the file
<div class="jumbotron"> <h1>Unauthorized</h1> <p>You are not in the required user roles to view this page.</p> </div>
authenticate_and_authorize behaves like
authorizedForAll (used above). The additional functionality is that it checks if the client has a valid SSO token, then checks to see if the logged in user also has the appropriate role(s). If the user does not have the appropriate role(s), it redirects the user to the
UnauthorizedController with an
Troubleshooting - If you are redirected to the unauthorized page, check to see that your employee is in the proper clinical roles in the Employees section of the Warehouse view of the Service Tools application. Once the employee is configured correctly, try clicking the "Worklist" link in the navigation to test again (a common mistake is refreshing the unauthorized page after fixing the employee-clinical role mappings, which will continue to show the unauthorized page instead of resubmitting the request for the Worklist).
Hiding page components by role
In addition to redirecting an unauthorized user, it is possible to show different fragments of a page dependent on employee-clinical role mappings. This can be done using if statements and the SDK authorization methods (like above), but you can also use the block helpers provided by
app/views/worklist/index.html.erb to conditionally add an extra column to the table (this column will be used in Part 7):
<div class="container-fluid"> <div class="row"> <div class="col-xs-12"> <h1>Ready to read worklist</h1> <table class="table table-bordered table-striped"> <thead> <tr> <th>Accession #</th> <th>Patient MRN</th> <th>Patient Name</th> <th>Completed At</th> <th>Age</th> <% authorized(["radiologist"]) do %> <th></th> <% end %> </tr> </thead> <tbody> <% @exams.each do |exam| %> <tr> <td><%= exam.accession %></td> <td><%= exam.patientMrn.mrn %></td> <td><%= exam.patientMrn.patient.name %></td> <td><%= formatd(exam.radExamTime.endExam) %></td> <td><%= age(exam.radExamTime.endExam) %></td> <% authorized(["radiologist","ai-staff"]) do %> <td><button class="btn btn-xs btn-primary">View Images</button></td> <% end %> </tr> <% end %> </tbody> </table> </div> </div> </div>
Note - The
authorizedblock helper is using a concat-based method to add the contents of the block to the page. Do not put an
erbtag, similar to the each expression.
This creates a button on the page that only "radiologists" and "ai-staff"-mapped employees will see.
authorized works just like
authorizedForAny. You can also use the
authorize_all, or the more verbose name for the same behavior as
any_authorized. In addition to the block helpers, there are other helpers that return booleans (like the SDK). They have the same names as the block helpers with a
? suffix (e.g.
Apps should always give the user a way to log out. This is done with a link that has the
href set to the value returned by the SDK method
app/views/layouts/application.html.erb to add the logout link in the primary navigation, under the "Worklist" link:
<li><a href="<%= Java::HarbingerSdk::SSO.logoutUrl() %>">Logout</a></li>