Skip to content

Authorization based on HTTP Basic Authentication

Vladimir Kotal edited this page Sep 29, 2020 · 19 revisions

This is an example of how to use the Authorization framework with HTTP Basic authentication using the Tomcat application server.

As mentioned in the Authorization wiki, OpenGrok does not perform authentication, just authorization. The authentication step is to be performed by one of the layers above OpenGrok, e.g. application server, reverse proxy, etc.

Assumptions

For this demonstration, we will need to have two projects defined - foo and bar, both containing a file with some of the content common. We will assume the same directory structure as described on https://github.com/opengrok/opengrok/wiki/How-to-setup-OpenGrok, i.e. the source root will be the /opengrok/src directory:

mkdir /opengrok/src/{foo,bar}
cd /opengrok/src
echo greenparrot > foo/data.txt
echo greenparrot > bar/data.txt

Setting up Tomcat

HTTP Basic authentication (as per RFC 7617) is supported by Tomcat. While in this particular case Tomcat can provide both authentication and authorization, it is recommended to always configure authorization in the OpenGrok web application as it is aware of all the places where authorization needs to be done which is something the application server could not know.

Tomcat users

In this example, we will define two users. Each user will have access just to the project matching his user name.

We have to modify a Tomcat file to provide information about users and roles in the system. This file is placed in $CATALINA_BASE/conf/tomcat-users.xml. For purpose of this example add these lines to the file inside the element <tomcat-users>. In its entirety the file will look like this:

<?xml version='1.0' encoding='utf-8'?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">

  <user username="foo" password="foo" roles="tomcat,manager-script"/>
  <user username="bar" password="bar" roles="tomcat,manager-script"/>
  <user username="root" password="root" roles="tomcat,manager-script"/>
</tomcat-users>

Application deployment descriptor

Now we have to tell the application that it should use HTTP Basic authentication to authenticate its sources. We can do this by modifying the web.xml file which is usually placed in the WEB-INF directory in your application. This directory is created once the web application is deployed.

Inserting the following lines into the web.xml file (under the top level web-app XML element) to make it work:

<security-constraint>
    <web-resource-collection>                                               
        <web-resource-name>API endpoints are checked separately by the web app</web-resource-name>
        <url-pattern>/api/*</url-pattern>                                   
    </web-resource-collection>                                              
</security-constraint>

<security-constraint>
    <web-resource-collection>
        <web-resource-name>In general everything needs to be authenticated</web-resource-name>
        <url-pattern>/*</url-pattern> <!-- protect the whole application -->
        <url-pattern>/api/v1/search</url-pattern> <!-- protect search endpoint whitelisted above -->
        <url-pattern>/api/v1/suggest/*</url-pattern> <!-- protect suggest endpoint whitelisted above -->
    </web-resource-collection>

    <auth-constraint>
        <role-name>*</role-name>
    </auth-constraint>

    <user-data-constraint>
        <!-- transport-guarantee can be CONFIDENTIAL, INTEGRAL, or NONE -->
        <transport-guarantee>NONE</transport-guarantee>
    </user-data-constraint>
</security-constraint>

<security-role>
    <role-name>*</role-name>
</security-role>

<login-config>
    <auth-method>BASIC</auth-method>
</login-config>

This configuration ensures that Tomcat will request the authentication for the specified URLs. The asterisks in the role-name elements will make Tomcat to perform authentication only. The fine grained authorization of authenticated users will be then performed by the OpenGrok web application itself. This is the recommended way how to configure authorization.

Keep in mind that you will need to reinsert these contents with each redeploy since the file will be overwritten with the contents in the source.war file. The opengrok-deploy script will help you there - just use the --insert option:

    opengrok-deploy --insert insert.xml source.war \
        /path/to/your/application/server/webapps/newsrc.war 

Note that the contents to be inserted has to be fully formed XML document. For the above example this needs wrapping the contents inside these elements:

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
   <!-- insert some elements here -->
</web-app>

Web application configuration

It is necessary to configure these:

  • plugin directory path: this is where plugins.jar file as found in the distribution archive needs to be placed
  • authorization stack(s)
  • whitelists: we are going to use an authorization plugin that is going to check the users against a whitelist to see if they are authorized to view given project(s). In this case the whitelists are stored in the /opengrok/etc directory. The whitelist is simple text file where each line is a user name.

Create the whitelists:

echo foo > /opengrok/etc/foo-whitelist.txt
echo root >> /opengrok/etc/foo-whitelist.txt
echo bar > /opengrok/etc/bar-whitelist.txt
echo root >> /opengrok/etc/bar-whitelist.txt

Now let's save the following into /opengrok/etc/readonly-configuration.xml (to be used as read-only configuration):

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_60" class="java.beans.XMLDecoder">
 <object class="org.opengrok.indexer.configuration.Configuration" id="Configuration0">

  <void property="pluginDirectory">
   <string>/opengrok/dist/share/lib</string>
  </void>

  <void property="pluginStack">
        <void property="stack">
            <!-- get user cred from HTTP headers -->
            <void method="add">
                <object class="org.opengrok.indexer.authorization.AuthorizationPlugin">
                    <void property="name">
                        <string>opengrok.auth.plugin.UserPlugin</string>
                    </void>
                    <void property="flag">
                        <string>REQUISITE</string>
                    </void>
                    <void property="setup">
                        <void method="put">
                             <string>decoder</string>
                             <string>opengrok.auth.plugin.decoders.UserPrincipalDecoder</string>
                        </void>
                    </void>
                </object>
            </void>

            <void method="add">
                <object class="org.opengrok.indexer.authorization.AuthorizationPlugin">
                    <void property="forProjects">
                            <void method="add">
                                <string>foo</string>
                            </void>
                    </void>
                    <void property="name">
                        <string>opengrok.auth.plugin.UserWhiteListPlugin</string>
                    </void>
                    <void property="flag">
                        <string>REQUIRED</string>
                    </void>
                    <void property="setup">
                        <void method="put">
                             <string>file</string>
                             <string>/opengrok/etc/foo-whitelist.txt</string>
                        </void>
                    </void>
                </object>
            </void>

            <void method="add">
                <object class="org.opengrok.indexer.authorization.AuthorizationPlugin">
                    <void property="forProjects">
                            <void method="add">
                                <string>bar</string>
                            </void>
                    </void>
                    <void property="name">
                        <string>opengrok.auth.plugin.UserWhiteListPlugin</string>
                    </void>
                    <void property="flag">
                        <string>REQUIRED</string>
                    </void>
                    <void property="setup">
                        <void method="put">
                             <string>file</string>
                             <string>/opengrok/etc/bar-whitelist.txt</string>
                        </void>
                    </void>
                </object>
            </void>
        </void>
  </void>
 </object>
</java>

The directory with authorization plugins is set to /opengrok/dist/share/lib/auth/plugins. Again, this is assuming the directory structure from https://github.com/opengrok/opengrok/wiki/How-to-setup-OpenGrok. This directory can contain both extracted plugin classes (in the respective directory structure) and JAR files with authorization plugins. In this case, the extracted distribution has a single plugins.jar file in the plugin directory.

Note that the first plugin in the stack was defined to use the HttpBasicAuthHeaderDecoder. This decoder converts the data from the HTTP Authorization header produced by Tomcat into an attribute carrying the user name in the request so that is available to other plugins down the stack.

Of course, this can be made more sophisticated using project groups. A combination of forProjects and forGroups properties can be used, making this truly flexible, not mentioning the possibility of adding more plugins or sub-stacks to the stack.

Deploy

If you have not deployed the web application before, do it now. See https://github.com/oracle/opengrok/wiki/How-to-setup-OpenGrok#step2---deploy-the-web-application for more info.

Index

Now run the Indexer. It is necessary to use the -R option with authorization stack configuration. For example the Indexer options will be:

-s
/opengrok/src
-d
/opengrok/data
-P
-H
-S
-G
-R
/opengrok/etc/readonly_configuration.xml
-W
/opengrok/etc/configuration.xml
-U
http://localhost:8080/source

Note that there are other ways how to make the read-only configuration to be put into effect without running the indexer. This was used just for simplicity. That said, keep in mind that the indexer has to be run with the -R option every time, unless use the project centric workflow using repository synchronization scripts.

At the end of the Indexer run, the web application will reload its configuration and the authorization stack will be put into effect. In the log file (e.g. catalina.out in Tomcat) the log entries will look like so:

26-Aug-2019 13:19:21.635 INFO [http-nio-8080-exec-3] org.opengrok.indexer.framework.PluginFramework.reload Plugins are being reloaded from /opengrok/dist/share/lib
26-Aug-2019 13:19:21.645 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationFramework.classLoaded plugin opengrok.auth.plugin.LdapUserPlugin is not configured in the stack
26-Aug-2019 13:19:21.651 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationFramework.classLoaded plugin opengrok.auth.plugin.LdapFilterPlugin is not configured in the stack
26-Aug-2019 13:19:21.653 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationFramework.classLoaded plugin opengrok.auth.plugin.TruePlugin is not configured in the stack
26-Aug-2019 13:19:21.668 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationFramework.classLoaded plugin opengrok.auth.plugin.HttpBasicAuthorizationPlugin is not configured in the stack
26-Aug-2019 13:19:21.678 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationFramework.classLoaded plugin opengrok.auth.plugin.FalsePlugin is not configured in the stack
26-Aug-2019 13:19:21.685 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationFramework.classLoaded plugin opengrok.auth.plugin.LdapAttrPlugin is not configured in the stack
26-Aug-2019 13:19:21.689 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationStack.load [REQUIRED] Stack "default stack" is loading.
26-Aug-2019 13:19:21.693 INFO [http-nio-8080-exec-3] opengrok.auth.plugin.UserPlugin.load loading decoder: opengrok.auth.plugin.decoders.UserPrincipalDecoder
26-Aug-2019 13:19:21.694 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationPlugin.load [REQUISITE] Plugin "opengrok.auth.plugin.UserPlugin" found and is working.
26-Aug-2019 13:19:21.695 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationPlugin.load [REQUIRED] Plugin "opengrok.auth.plugin.UserWhiteListPlugin" found and is working.
26-Aug-2019 13:19:21.696 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationPlugin.load [REQUIRED] Plugin "opengrok.auth.plugin.UserWhiteListPlugin" found and is working.
26-Aug-2019 13:19:21.696 INFO [http-nio-8080-exec-3] org.opengrok.indexer.authorization.AuthorizationStack.load [REQUIRED] Stack "default stack" is ready.

Test

Once the indexer completes its work and sends the new configuration to the web application, the authorization configuration will be put into effect. We can test using both web UI and also the RESTful API.

Using web UI

For example in Firefox, open new Private window, enter user credentials and check the list of projects on the index page of the web application.

Using RESTful API

We will need base 64 encoded username and password since this is how HTTP Basic authentication works:

$ python3
Python 3.6.8 (default, Jan 14 2019, 11:02:34) 
[GCC 8.0.1 20180414 (experimental) [trunk revision 259383]] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import base64
>>> base64.encodestring(b"foo:foo")
b'Zm9vOmZvbw==\n'
>>> base64.encodestring(b"root:root")
b'cm9vdDpyb290\n'
>>> base64.encodestring(b"bar:bar")
b'YmFyOmJhcg==\n'

Then we can use these to perform the queries. The foo and bar projects were deliberately set to contain a file called data.txt with the same content (string greenparrot) so let's see if the authorization stack works:

# try user foo
$ curl -H 'Authorization: Basic Zm9vOmZvbw==' -s 'http://10.x.y.z:8080/source/api/v1/search?full=greenparrot' | jq
{
  "time": 38,
  "resultCount": 1,
  "startDocument": 0,
  "endDocument": 0,
  "results": {
    "/foo/data.txt": [
      {
        "line": "<b>greenparrot</b>",
        "lineNumber": "1"
      }
    ]
  }
}
# try user bar
$ curl -H 'Authorization: Basic YmFyOmJhcg==' -s 'http://10.x.y.z:8080/source/api/v1/search?full=greenparrot' | jq
{
  "time": 74,
  "resultCount": 1,
  "startDocument": 0,
  "endDocument": 0,
  "results": {
    "/bar/data.txt": [
      {
        "line": "<b>greenparrot</b>",
        "lineNumber": "1"
      }
    ]
  }
}
# try user root
$ curl -H 'Authorization: Basic cm9vdDpyb290' -s 'http://10.x.y.z:8080/source/api/v1/search?full=greenparrot' | jq
{
  "time": 12,
  "resultCount": 2,
  "startDocument": 0,
  "endDocument": 1,
  "results": {
    "/bar/data.txt": [
      {
        "line": "<b>greenparrot</b>",
        "lineNumber": "1"
      }
    ],
    "/foo/data.txt": [
      {
        "line": "<b>greenparrot</b>",
        "lineNumber": "1"
      }
    ]
  }
}

Notice that for the RESTful API call to the /search endpoint, no projects were specified, so the web app searches all projects the user is authorized to. As expected, user foo is able to see/search project foo, user bar project bar and user root both projects.