Creating a secure Roxy application

So I’ve made a previous post about Roxy where I mentioned that by default there’s no support for protected URLs. Well after a little thinking, help from the internal MarkLogic mailing list, and some testing I’ve come up with a solution. I detail it here in this blog entry.

The first thing you need to do is understand routes. As I’ve mentioned before, a route maps a URL to a particular controller function and view. So this URL:-

/analysis/search.html

Would invoke the search() function in the /app/analysis.xqy controller, and render the /app/views/analysis/search.html.xqy view. Very Ruby on Rails like. What it will also do is route public URLs like these:-

/css/style.css -> /public/css/style.css
/images/someimage.png -> /public/images/someimage.png

All fine, hip and groovy. What if you need or want to lock down particular URLs to particular users? Or just prevent the ‘default user’ from accessing any content?

MarkLogic by default has HTTP digest authentication turned on. This means before a page is rendered, your web browser will display a username and password popup. Not the most beautiful of pages, and doesn’t allow for any public content.

Happily MarkLogic Server has a solution. You can change authentication mode to ‘Application’ and set that application’s default user. Typically this is a user without access to any content. Roxy creates one of these for you, your <appname>-user account.

Once this is done, anyone accessing your application is treated as your <appname>-user. So they’ll see the /analysis/search.html page or be able to type it in, but no content from the MarkLogic server will be shown – you’ll get blank search results each time. Half way there at least.

The next thing we need to do is instruct Roxy as to which URLs, and thus controllers, functions, etc.) can be accessed. Afterall, no point giving users access to a page they can’t see anything on. Also, you may not even want unauthenticated users to realise what capabilities the application would allow if only they had access.

The way we do this is to configure Roxy. Lets take a look at the default Roxy routes from /roxy/config/defaults.xqy – warning: DO NOT EDIT this file:-

declare variable $ROXY-ROUTES :=
(: The default Roxy routes :)
let $default-controller :=
(
 $config:ROXY-OPTIONS/*:default-controller,
 $ROXY-OPTIONS/*:default-controller
)[1]
let $default-function :=
(
 $config:ROXY-OPTIONS/*:default-function,
 $ROXY-OPTIONS/*:default-function
)[1]
return
 <routes xmlns="http://marklogic.com/appservices/rest">
  <request uri="^/test/(js|img|css)/(.*)" />
  <request uri="^/test/(.*)" endpoint="/test/default.xqy">
    <uri-param name="func" 
      default="{$default-function}">$1</uri-param>
  </request>
  <request uri="^/test$" redirect="/test/" />
  <request uri="^/(css|js|images)/(.*)" endpoint="/public/$1/$2"/>
  <request uri="^/favicon.ico$" endpoint="/public/favicon.ico"/>
  <request uri="^/([\w\d_\-]*)/?([\w\d_\-]*)\.?(\w*)/?$" 
    endpoint="/roxy/query-router.xqy">
   <uri-param name="controller" 
    default="{$default-controller}">$1</uri-param>
   <uri-param name="func" 
    default="{$default-function}">$2</uri-param>
   <uri-param name="format">$3</uri-param>
   <http method="GET"/>
   <http method="HEAD"/>
  </request>
  <request uri="^/([\w\d_\-]*)/?([\w\d_\-]*)\.?(\w*)/?$" 
    endpoint="/roxy/update-router.xqy">
   <uri-param name="controller" 
    default="{$default-controller}">$1</uri-param>
   <uri-param name="func" default="{$default-function}">$2</uri-param>
   <uri-param name="format">$3</uri-param>
   <http method="POST"/>
   <http method="PUT"/>
   <http method="DELETE"/>
  </request>
  <request uri="^.+$"/>
 </routes>;

As you can see, public files are served first, then GET and HEAD requests to controllers are handle by the query router, and POST, PUT and DELETE requests to controllers are handled by the update router.

What we need to do is override this in our /app/config/config.xqy module:-

 declare variable $default-controller := "appbuilder";
 declare variable $default-function := "main";
 declare variable $c:ROXY-ROUTES :=
  <routes xmlns="http://marklogic.com/appservices/rest">   
  {
  let $user := xdmp:get-current-user()
  return
    if ($user ne '
      <myappname>-user') then
      $def:ROXY-ROUTES/rest:request
    else

      <request uri="^/(css|js|images)/(.*)" endpoint="/public/$1/$2"/>,
      <request uri="^/favicon.ico$" endpoint="/public/favicon.ico"/>,
      <request uri="^/sec/login" endpoint="/roxy/query-router.xqy">
        <uri-param name="controller" default="{$default-controller}">sec</uri-param>
        <uri-param name="func" default="{$default-function}">login</uri-param>
        <uri-param name="format">html</uri-param>
        <http method="GET"/>
        <http method="HEAD"/>
      </request>,
      <request uri="^/.*$" endpoint="/roxy/query-router.xqy">
        <uri-param name="controller" default="{$default-controller}">appbuilder</uri-param>
        <uri-param name="func" default="{$default-function}">main</uri-param>
        <uri-param name="format">html</uri-param>
        <http method="GET"/>
        <http method="HEAD"/>
      </request>,

      $def:ROXY-ROUTES/rest:request
    }
    <request uri="^/my/awesome/route" />
    {
      $def:ROXY-ROUTES/rest:request
    }
  </routes>;

As you can see, if the username of the user matches our default user, i.e. the user hasn’t logged in yet, then they can only access public resources (css, images, etc.), the main page (appbuilder/main.html is the same as the / url in Roxy) and of course the URL that handles a login request. This should really be a POST so as not to reveal the username and password in the URL.

Note in my application the login dialog is always shown via the layout I’m using. If the user is the default user, a login page is shown. Otherwise you see something like ‘Welcome Adam’ and a link to their profile page. Both these pieces of functionality are provided out of the box in Roxy, in the /app/views/helpers/user-lib.xqy module’s uv:build-user function. This function takes several parameters, including username (I pass in blank when it’s my default user), and links for the login action URL, profile URL, register URL (if applicable) and logout URL. Very handy.

Now you just need a security controller. I’ve just built my own that executes MarkLogic Server’s default user authentication mechanisms. Here’s my controller (/app/controllers/sec.xqy):-

xquery version "1.0-ml";

module namespace c = "http://marklogic.com/roxy/controller/sec";

(: the controller helper library provides methods to control which view 
 : and template get rendered :)
import module namespace ch = "http://marklogic.com/roxy/controller-helper" 
  at "/roxy/lib/controller-helper.xqy";

(: The request library provides awesome helper methods to abstract 
 : get-request-field :)
import module namespace req = "http://marklogic.com/roxy/request" 
  at "/roxy/lib/request.xqy";

declare option xdmp:mapping "false";

(:
 : Usage Notes:
 :
 : use the ch library to pass variables to the view
 :
 : use the request (req) library to get access to request parameters easily
 :
 :)
declare function c:login() as item()*
{
  let $user := xdmp:get-request-field("username")
  let $pass := xdmp:get-request-field("password")
  let $login := xdmp:login($user,$pass)

  return
    ch:add-value("success",$login),
  ch:add-value("message", "This is a test message."),
  ch:add-value("title", "This is a test page title"),
  ch:use-view((), "xml"),
  ch:use-layout((), "xml")
};

declare function c:logout() as item()*
{
  let $logout := xdmp:logout()
  return
    xdmp:redirect-response("/")
(:
  ch:add-value("message", "This is a test message."),
  ch:add-value("title", "This is a test page title"),
  ch:use-view((), "xml"),
  ch:use-layout((), "xml")
:)
};

declare function c:profile() as item()*
{
  ()
(:
  ch:add-value("message", "This is a test message."),
  ch:add-value("title", "This is a test page title"),
  ch:use-view((), "xml"),
  ch:use-layout((), "xml")
:)
};

Note I’m not currently displaying anything for the profile page. My login view page just says ‘You are now logged in’. Note also that the logout function forwards the user to the main function in the appbuilder controller (via the / URL). Very lightweight, but works fine for my needs. Obviously you’ll need to do some security testing yourselves to ensure I’ve not missed any glaring exploits.

What I’ve also done in my app is edit the layout code so extra menu links only appear when the user is logged in. No point showing navigation when it can’t be used. Here’s an excerpt from my layout. Firstly to see if its the default user:-

declare variable $username as xs:string? := 
  if (xdmp:get-current-user() = "-user") then () else xdmp:get-current-user();

Then within the html a check for this being non empty;-

{if ($username) then
  <li class="hc"><a href="/analysis/search">Analysis</a></li>
else ()
}

Just for completeness, here is my call to build the login form, also within the layout module:-

{
  uv:build-user($username, fn:concat("/sec/profile?user=", $username), 
    "/sec/login", "/sec/register", "/sec/logout")
}

Hopefully this will be useful to anyone else working with Roxy. I’m going to show this to our Roxy guys to see if they want to expand on this for a future release. E.g. have a configuration option for ‘enable URL security’ and another for ‘public URLs’ perhaps.

Let me know on here if you have found this useful!

2 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.