This is the developer documentation for fabric8-auth. It is useful if you wish to contribute to the fabric8-auth project in some way.

Source code

The latest source code for fabric8-auth can be found at GitHub here. The repository is configured to perform automatic, continual releases from the master branch any time a new pull request is merged.

Issue Tracking

GitHub issues are used for tracking the development of new features and service issues.

Development workflow

Basically, all work done to the fabric8-auth code base must have a corresponding issue, whether it be a complex new feature, major or minor bug or simply correcting a typo in the code, readme file or documentation. This allows the project team to carefully manage and audit the changes made to such a critical piece of the fabric8 platform. If you wish to contribute any code or other changes to the project, here are the steps to follow:

  1. Pick an existing issue to work with (or create a new one)

  2. Fork the fabric8-auth repository to your own GitHub account

  3. Check out the forked repository in your development environment

  4. Create a new branch representing the issue you will work on (git checkout -b ISSUE-XXX)

  5. Make your changes, commit them to the issue branch

  6. Push the changes to your forked repository

  7. In Github, browse to the issue branch in your forked repository

  8. Create a pull request for your issue branch, making sure to clearly link the issue that the PR is for

  9. Update the issue with a link to the pull request

Makefile targets

build

Compiles sources into executable binary

dev

Same as build, however also starts up a Docker instance of the Postgres database and runs the auth service on the host.

deploy-auth-openshift

Starts a minishift instance and deploys the "latest" tagged versions of the fabric-auth service, plus a postgres database from the docker.io repository, exposing the service at http://minishift.local:31000.

dev-db-openshift

Starts a minishift instance and deploys a postgres database to it, while building and running the fabric8-auth service on the host machine which then connects to the db on port 31001.

dev-openshift

Starts a minishift instance and deploys a postgres database to it, then builds and deploys the fabric-auth service to minishift also, exposing the service at http://minishift.local:31000.

clean-openshift

Removes the deployment from minishift, deleting the deployments and removing the project.

docs

Builds the documentation in the /docs directory.

Service Accounts

Service accounts are used by other services to allow secure service-to-service interaction. One example of this is the auth service’s protection API, which requires a PAT (Protection API Token) in order for the resource servers to register newly created resources, and query the access rights for clients attempting to access resources that they don’t own.

Service account configuration is expected to be found in /etc/fabric8/service-account-secrets.conf, in JSON format. Here’s an example configuration:

{
    "accounts": [
        {
            "name":"fabric8-wit",
            "id":"5dec5fdb-09e3-4453-b73f-5c828832b28e",
            "secrets":["$2a$04$nI7z7Re4pbx.V5vwm14n5.velhB.nbMgxdZ0vSomWVxcct34zbH9e"]
        },
        {
            "name":"fabric8-tenant",
            "id":"c211f1bd-17a7-4f8c-9f80-0917d167889d",
            "secrets":["$2a$04$ynqM/syKMYowMIn5cyqHuevWnfzIQqtyY4m.61B02qltY5SOyGIOe", "$2a$04$sbC/AfW2c33hv8orGA.1D.LXa/.IY76VWhsfqxCVhrhFkDfL0/XGK"]
        },
        {
            "name":"fabric8-jenkins-idler",
            "id":"341c283f-0cd7-48a8-9281-4583aceb3617",
            "secrets":["$2a$04$hbGHAVKohpeDgHzafnLwdO4ZzhEn9ukVP/6CaOtf5o3Btp.r6tXTG"]
        }
    ]
}

At deployment time this configuration file is provided by the container, via a pre-configured secret (see the reference docs for details). The configuration file is loaded in /configuration/configuration.go as part of the call to NewConfigurationData.

This method loads the service account configuration and stores it in the ConfigurationData.sa type variable.

The path of the configuration file can be overridden by either setting a command line parameter (serviceAccountConfig) or setting an environment variable (AUTH_SERVICE_ACCOUNT_CONFIG_FILE) to point to a new file location (see the main() function in /main.go).

Authentication

Service account authentication is handled by the Exchange function in /controller/token.go:

func (c *TokenController) Exchange(ctx *app.ExchangeTokenContext) error {

Permission Checks

The HasScope() method provided by the permission service makes use of a complex SQL query in order to determine whether a user has a particular scope for a resource. Besides checking for privileges assigned directly to the user, this query also takes into account:

  • that the user may inherit the scope indirectly via their memberships in an organization, team or security group

  • that the resource in question may have a parent resource (or other resource ancestry) of the same type to which the user has been granted privileges

  • that there may be a role mapping, allowing a user that has been granted privileges to a resource of a different type somewhere in the resource ancestry to gain privileges for other resource types further down the hierarchy

It accepts three parameters; the user’s identity ID, the resource ID and the name of the scope.

This section will describe the various parts of the permission query, providing a useful reference in case the SQL requires future maintenance.

To begin, let’s examine the query at the highest level. Essentially, it checks the IDENTITY_ROLE table for the existence of any records that match the criteria as described above. It does this by setting conditions for the three columns; identity_id, resource_id and role_id, which can be seen as follows:

permission has scope sql 1

The following sections will examine each of these sections in more detail.

Identity criteria

In section 1 of the query above, the condition limits the records returned to those with an IDENTITY_ID equal to the current user’s, or to that of any organization, team or security group in which the current user is a member.

This is done by selecting a list of the corresponding ID values from the identities table, and matching the identity_role.identity_id value against these:

permission has scope sql 2
  1. The first condition is a direct check for the user’s identity ID, to match any records for which the role has been assigned directly to the user.

permission has scope identity role identity
  1. The second condition includes the identity ID for any organization, team or security group in which the user is either directly or indirectly a member. This is achieved by executing a recursive subquery in the membership table to "walk" up the user’s membership hierarchy, and extract out all of the member_of values. This allows for a permission check in which the user may inherit privileges for the specified resource indirectly via their memberships.

For example, the privileges for a user which is a member of a team, which belongs to an organization could be represented like this:

permission has scope identity role memberships

Resource criteria

The second condition of the query is used to select records for the specified resource. Its criteria restricts records to those with either the exact same resource_id value as the resource parameter, or to an ancestor resource (since resources inherit privileges from their parent):

permission has scope sql 3

The criteria does this by executing a recursive subquery on the resource hierarchy, using the resource table’s parent_resource_id column to walk up the hierarchy. The resulting list of resource IDs is used to restrict the resource_id column.

permission has scope identity role resource

Role criteria

The third condition of the query is by far the most complex, and is used to construct the set of valid roles matching the specified scope parameter. This condition has been broken down into smaller sections which are described in more detail below.

permission has scope sql 4

Section 1

The first criteria selects roles that have the same resource type as the resource parameter, and the same scope as the scope parameter. It matches on roles assigned both directly to the user, or indirectly via the user’s memberships, that have the specified SCOPE value, that have been directly assigned for the resource specified by the RESOURCE_ID parameter.

permission has scope sql role 1
permission has scope sql role 1 resource scope

Section 2

The second criteria is quite simple; the CROSS JOIN LATERAL clause takes the results from the from_role_id and to_role_id columns returned by the recursive inner query (see next section), and combines them into a single column called role_id. The DISTINCT keyword is then used to remove duplicates from this list. This list of role_id values is then used as an IN condition for the IDENTITY_ROLE.ROLE_ID column.

Section 3

The next part of the query gets a little more tricky. This complex recursive query does quite a few things - let’s start with the first subquery:

permission has scope sql role 3 part1

This subquery returns the base set of results upon which the recursion will occur in the second section. It searches for role mappings that a) apply to the resource specified by the RESOURCE_ID parameter; and b) map to a role that has the specified SCOPE parameter.

permission has scope sql role 3 role mapping scope

Why does the query need this? Well, if the user has been granted a different role for a resource higher up the resource hierarchy, but that role has been mapped via a role mapping to another role that has the scope that we’re interested in for the specified resource, we first need to build a list of role mappings that actually map to any roles that grant this privilege. This is what this first section of the recursive query does.

Since we are actually interested in all role mappings for the resource’s ancestor hierarchy, we need to query the resource’s hierarchy here also - this is covered in the next section.

To illustrate more clearly how role mappings are queried, we will look at an example.

Let us imagine that we have a resource type called codebase, and that we have a single codebase resource. The codebase resource type also has one scope, commit, which has been assigned to the developer role:

permission has scope sql role example 1

Now let us imagine that we have another resource type called space, and that we also have a single space resource, and that the space resource type also has a role called contributor:

permission has scope sql role example 2

So far so good. Now let’s also say that a codebase resource always has a space resource as its parent. We can represent it like this:

permission has scope sql role example 3

Next, let’s add a user and grant them the contributor role for the space resource. This is done by creating an identity_role entry that links the user, resource and role:

permission has scope sql role example 4

Now we come to role mappings - let’s say that for any user that has the contributor role for our space resource, we wish to automatically grant them the developer role for any child codebase resources. In fact, the codebase resource doesn’t need to be a child, it can be a grandchild or great grandchild, i.e. it simply needs to exist as a descendent somewhere in the resource hierarchy. To achieve this, we create a role mapping for our resource, that maps the contributor role to the developer role:

permission has scope sql role example 5

Let’s put it all together. Since our user has been granted the contributor role for the space resource, and since there is a role mapping for our resource that maps the contributor role to the developer role, our user automatically gains the developer role for any descendent codebase resources:

permission has scope sql role example 6

It is our query’s task to locate the role mappings that grant a user the specified SCOPE parameter for the specified resource. With this in mind, our first subquery in this section of the query identifies any role mappings that directly map to any of the roles that grant the specified SCOPE:

permission has scope sql role example 7

Let’s now look at the second part of our recursive query (ignoring section 4 and 5 for now as they are described below):

permission has scope sql role 3 role mapping hierarchy

The overall goal of the recursive query is to locate role mappings that map to roles that grant the specified SCOPE parameter value, and to then use that list of role mappings as criteria for the permission query. There are two possibilities here:

  • the user (or any organization, team or group in which they are a member) has been directly granted a role that maps to another role that has the desired scope, or

  • the user (or their organization, etc) has been granted a role that maps to a role further down the resource hierarchy, and that role is further mapped down the hierarchy to the role that we’re interested in.

The first possibility is already addressed by the first part of the hierarchical query. This leaves the second scenario, a multiple level role mapping hierarchy, which is dealt with by the second part of the recursive query. Let’s start by visualizing a special example of this, in which we assign each resource and role a unique identifier:

permission has scope sql role example 8

We can see that our user jane_smith has been granted the employee role, and that there is a role mapping that maps the employee role to the contributor role, and another role mapping that maps the contributor role to the developer role. Since the first part of the recursive query has already identified the role mapping we are interested in (in this case, the one that maps role B to role C), we simply need to walk up the role mapping hierarchy. In section 5, a list of resource IDs for the role mapping is generated in a recursive subquery. That just leaves one task for the second part of the recursive query, select a list of the role mappings that themselves chain together within the resource’s ancestor hierarchy.

Since we have two role mappings here, we can represent their data as such:

RESOURCE_ID

FROM_ROLE_ID

TO_ROLE_ID

R1

A

B

R2

B

C

Since we are joining by FROM_ROLE_ID = TO_ROLE_ID, and starting from the bottom up, we get a match in that the R2 role mapping’s FROM_ROLE_ID equals R1 role mapping’s TO_ROLE_ID, so the R1 role mapping gets selected.

What we end up with when both the first and second parts of the recursive query get UNIONed together, is a list containing the FROM_ROLE_ID and TO_ROLE_ID values from both role mappings:

FROM_ROLE_ID

TO_ROLE_ID

A

B

B

C

As was previously explained in Section 2, these results are then combined into a single column of unique values which are then used as a condition for the IDENTITY_ROLE.ROLE_ID criteria.

Section 4

This section of the query produces a list of all the resource ID values in the RESOURCE_ID parameter’s parent hierarchy, exactly the same way as described above in the Resource Criteria section. It is used as a subquery for the first part of the hierarchical role mapping query described in Section 3.

permission has scope sql role 3 part4

Section 5

This recursive subquery produces a list of all the resource ID values for the selected role mapping’s resource hierarchy. It is used as a condition to restrict the search for role mappings within the same resource hierarchy, as explained above in Section 3.

permission has scope sql role 3 part5