Thursday, February 18, 2016

Revealing OpenAM's Policy Enforcement Model

In my post on migrating OAM 10g policies to OpenAM I promised to outline how I clarified my understanding of OpenAM's policy enforcement model. The policy evaluation endpoint was the quickest way to test. But right up front you quickly learn that an authenticated user token is required. If it isn't included your results are always negative. So to test I always have an authenticated user token.

When considering policy decisions I have the following variables to consider after assuming that a request URL matches a given policy's resource URL.

  1. subject requirement is satisfied
  2. condition exists
  3. condition requirement is satisfied
  4. corresponding action is enabled
  5. action allows access
So five variables initially. If I start with a policy that has no condition then this drops to three variables. 

First Test: No Conflicting Action Outcomes

If we look at this as a truth table with zero being false and one being true we then need to test the following combinations and see what their outcome is. For the subject satisfied I used a user and group subject with user = demo and I had two users in OpenAM with usernames of demo and test. As noted to the left of the table, test represents subject satisfied being false and demo to it being true.

I also noted that when an action matching that of a request is not enabled the policy essentially doesn't match and makes no contribution to the outcome. What that meant for output results I didn't know prior to the test.

            action allows --------+
                                  |
         action enabled ------+   |
                              |   |
      subject satisfied --+   |   |
                          |   |   |
                          v   v   v
      - - - - - - - -   +---+---+---+
                        | 0 | 0 | 0 | no match
                        +---+---+---+
        test user       | 0 | 0 | 1 | no match
                        +---+---+---+
                        | 0 | 1 | 0 | deny
                        +---+---+---+ 
                        | 0 | 1 | 1 | allow
      - - - - - - - -   +---+---+---+
                        | 1 | 0 | 0 | no match
                        +---+---+---+
        demo user       | 1 | 0 | 1 | no match
                        +---+---+---+
                        | 1 | 1 | 0 | deny
                        +---+---+---+
                        | 1 | 1 | 1 | allow
      - - - - - - - -   +---+---+---+

For testing deny and allow I crafted the following two policies.

name: test-allow
URL: http://test.org/allow
subject: user = demo
actions: GET, POST, PUT enabledallow checked.  

name: test-deny
URL: http://test.org/deny
subject: user = demo
actions: GET, PUT enableddeny checked; POST enabledallow checked

Then with a nodejs script that reads a file of usernames and passwords and another file that specifies a list of URLs, I evaluated access via the policy evaluation rest endpoint for test and demo against the following URLs. That third one was intentionally meant not to match any policy to see what results it provided.

  1. http://test.org/allow
  2. http://test.org/deny
  3. http://test.org/no-match

The console output from running the script is shown below. The additional authentication is for the admin user that can hit the rest endpoint. In following output I'll only include the json results.



localhost:policy-tester boydmr$ node PT.js
loading ptConf.json
using cookie name of: iPlanetDirectoryPro
loading users file: ./users.json
loading policies file: urls.json
---> calling: http://ident-local.lds.org:8080/sso/json/authenticate
---> calling: http://ident-local.lds.org:8080/sso/json/authenticate
---> calling: http://ident-local.lds.org:8080/sso/json/authenticate
---> Acquired token.
---> Acquired token.
---> Acquired token.
{
    "user": "demo",
    "result": [
        {
            "http://test.org/allow": {
                "POST": true,
                "GET": true,
                "PUT": true
            }
        },
        {
            "http://test.org/no-match": null
        },
        {
            "http://test.org/deny": {
                "POST": true,
                "GET": false,
                "PUT": false
            }
        }
    ]
}
{
    "user": "test",
    "result": [
        {
            "http://test.org/allow": null
        },
        {
            "http://test.org/no-match": null
        },
        {
            "http://test.org/deny": null
        }
    ]
}

What I learned from this test is:
  1. Only methods that are enabled are returned in the result set.
  2. Where an action is marked to deny, a value of false for that action is returned.
  3. Where an action is marked to allow, a value of true for that action is returned.
  4. Where there is no policy matching a request either due to subject not satisfied or URL not matching a null object is returned. 
  5. When a null object is returned or a value of false is returned for an action the result should be to deny access to the request.

Second Test: When Action Outcomes Conflict

I then wanted to find out what happens when two policies match on URL, subject, and enabled action but the outcomes of that action conflict: allow versus deny. I created the following policy and ran the test again. As you can see from the highlighted portion, when allow and deny conflict, deny wins.

name: test-conflict
URL: http://test.org/all*
subject: user = demo
actions: POST enabled, deny checked. 

{
    "user": "test",
    "result": [
        {
            "http://test.org/allow": null
        },
        {
            "http://test.org/no-match": null
        },
        {
            "http://test.org/deny": null
        }
    ]
}
{
    "user": "demo",
    "result": [
        {
            "http://test.org/allow": {
                "POST": false,
                "GET": true,
                "PUT": true
            }
        },
        {
            "http://test.org/no-match": null
        },
        {
            "http://test.org/deny": {
                "POST": true,
                "GET": false,
                "PUT": false
            }
        }
    ]
}

Third Test: Resource Matches, Subject Doesn't

I changed the test-conflict policy to match the test user in the subject. The results now show as follows. Note that the conflict is no longer occurring and POST for the demo user is now true.

name: test-conflict
URL: http://test.org/all*
subject: user = test
actions: POST enableddeny checked. 



{
    "user": "demo",
    "result": [
        {
            "http://test.org/allow": {
                "POST": true,
                "GET": true,
                "PUT": true
            }
        },
        {
            "http://test.org/no-match": null
        },
        {
            "http://test.org/deny": {
                "POST": true,
                "GET": false,
                "PUT": false
            }
        }
    ]
}
{
    "user": "test",
    "result": [
        {
            "http://test.org/allow": {
                "POST": false
            }
        },
        {
            "http://test.org/no-match": null
        },
        {
            "http://test.org/deny": null
        }
    ]
}

Final Test: Using Conditions & Narrowing Access

Once I understood OpenAM's policy matching and enforcement model I now turned to one last hurdle that was critical to solve before we could migrate our policies from OAM to OpenAM. When a narrowly scoped policy is at a high point such as the root of a domain like test.org/* and a policy lower down such as test.org/stuff/* broadened access, the outcome is answered above; the actions will be aggregated and hence the broader audience will indeed have access only in the lower URL space.

But what of the opposite? Can we have a broad subject set at the top such as Authenticated Users and narrow scope below to a smaller set of users perhaps via a condition? As I thought about this the deny outcome of actions seemed to suddenly jump out at me. I could define two policies at that lower location. The first would grant access to the narrowed set. That alone would make no difference since the higher policy's outcomes would be additive and everyone would have access. But if the higher policy weren't there we would need it to explicitly open up access for the target audience. The key to answering this puzzle was the second policy. It was identical except in two aspects: it would wrap the condition in a logical NOT and change the action outcome to deny.

To test this I crafted the following three policies.

name: narrow-broad-top
URL: http://narrow.org/*
subject: Authenticated Users
actions: GET enabledallow checked.

name: narrow-restricted
URL: http://narrow.org/stuff/*
subject: Authenticated Users
condition: ldap-filter: (role=admin)
actions: GET enabledallow checked. 

name: narrow-restricted-deny
URL: http://narrow.org/stuff/*
subject: Authenticated Users
condition: NOT( ldap-filter: (role=admin) )
actions: GET enableddeny checked. 

The problem is I needed to have an ldap server to test these policies. Turns out this was simple to provide.

A Hand Crafted LDAP Server

I just happened to have run across a sweet nodejs library not too long ago that came to mind. It is ldapjs found at http://ldapjs.org/index.html. In about 30 minutes I had a prototype mock LDAP server whose source is included at the end of this post. And ldapsearch thought it was talking to a real ldap server. (See the comment section of the source.) The question was would OpenAM?

Activating LDAP Filter Use in Policy Evaluation

To use LDAP filters in policies in a realm OpenAM must first be told where the LDAP server is located to which it can defer evaluation of those filters. This is done on the realm in its Policy Configuration service. I configured mine with the following values based upon the LDAP server source for the bind user, object classes for users, and the search attribute.

Primary LDAP Server: 127.0.0.1:1389
User base DN: o=lds
Roles base DN: o=lds
Bind DN: cn=root
Bind Password: secret
Org search filter: (objectclass=sunismanagedorganization)
User search filter: (objectclass=inetorgperson)
User search scope: SUB
Role search scope: SUB
User search attribute: cn
all other values left as defaults.

Once it was running I was ready to try my test. When the policy evaluations executed the calls to the LDAP server appeared on its console indicating as desired that only the demo user matched the filter.

localhost:ldapjs-eval boydmr$ node server.js
users = {
  "demo": {
    "dn": "cn=demo, ou=users, o=lds",
    "attributes": {
      "cn": "demo",
      "uid": "demo",
      "role": "admin",
      "objectclass": "inetorgperson"
    }
  },
  "test": {
    "dn": "cn=test, ou=users, o=lds",
    "attributes": {
      "cn": "test",
      "uid": "test",
      "objectclass": "inetorgperson"
    }
  }
}
test LDAP server up at: ldap://0.0.0.0:1389
--   bind: cn=root
--   bind: cn=root
-- C-0 search: (&(objectclass=inetorgperson)(cn=demo)(role=admin))
-- C-0         demo = true
-- C-1 search: (&(objectclass=inetorgperson)(cn=test)(role=admin))
--   bind: cn=root

-- C-2 search: (&(objectclass=inetorgperson)(cn=test)(role=admin))

Conclusions


The results of test are shown below. Note that we do indeed narrow access for the demo user how has a role of admin while the test user does not. By defining these tests and then gathering their results from the policy evaluation rest endpoint I now have a clearly defined path for migrating out policies from OAM to OpenAM. Two key aspects that I have not covered of that strategy is how authorization expressions are translated and how OAMs URLs are translated in view of their richer meta character patterns. I'll see if I can't cover those topics soon.

{
    "user": "demo",
    "result": [
        {
            "http://narrow.org/stuff/for/admins": {
                "GET": true
            }
        },
        {
            "http://narrow.org/something": {
                "GET": true
            }
        }
    ]
}
{
    "user": "test",
    "result": [
        {
            "http://narrow.org/stuff/for/admins": {
                "GET": false
            }
        },
        {
            "http://narrow.org/something": {
                "GET": true
            }
        }
    ]
}



LDAP Server Source

/** * Quick and simple ldap server with two users having cn of: demo, and test. You can search for any of * them with: * * ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=lds" cn=<cn-here>
 * * Or search for all of them with: * * ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=lds" objectclass=* * * Created by boydmr on 2/4/16. */
var ldap = require('ldapjs');
//var express = require('expressjs');
var server = ldap.createServer();

// map of user attributes by cnvar users = {};
var cn = 'demo';

function createUser(cn, role) {
  users[cn] = {
    dn: 'cn=' + cn + ', ou=users, o=lds',
    attributes: {
      cn: cn,
      uid: cn,
      role: role,
      objectclass: 'inetorgperson'    }
  };
}

createUser('demo', 'admin');
createUser('test');

console.log("users = " + JSON.stringify(users, null, 2));

// add support for bindingserver.bind('cn=root', function(req, res, next) {
  if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') {
    // log failed attemtps to bind    console.log("!    bind: " + req.dn.toString() + " pwd: " + req.credentials);
    return next(new ldap.InvalidCredentialsError());
  }

  // log that we were bound to successfully and show username  console.log("--   bind: " + req.dn.toString());
  res.end();
  return next();
});

// filter that ensures we only handle non-anonymous requestsfunction authorize(req, res, next) {
  if (!req.connection.ldap.bindDN.equals('cn=root'))
    return next(new ldap.InsufficientAccessRightsError());

  return next();
}

var connId = 0;

server.search('o=lds', authorize, function(req, res, next) {
  var cid = 'C-' + connId++;
  console.log("-- " + cid + " search: " + req.filter.toString());

  Object.keys(users).forEach(function(k) {
    if (req.filter.matches(users[k].attributes)) {
      console.log("-- " + cid + "         " + k + " = true");
      res.send(users[k]);
    }

  });

  res.end();
  return next();
});

// now start listeningserver.listen(1389, function() {
  console.log('test LDAP server up at: %s', server.url);
});


Wednesday, February 17, 2016

Migrating OAM Policies to OpenAM

The policy enforcement models differ significantly between Oracle Access Manager 10g and OpenAM. To be clear, I'm referring only to using reverse proxy web gates (OAM's term) and agents (OpenAM's term) that protect access via policies to back-end application servers. In this post I'll start outlining how to migrate policies from OAM 10g to OpenAM. And for the record, i'm going to admit to some conceptual mistakes in this post and some may say, "Hey, why didn't you read the documentation?" And the answer is, I did.

Finding a Policy for a Request

Lets first discuss how a policy or policies are found to determine access for a given inbound request.

The OAM 10g Model

OAM uses policy domains that are containers of policies and have associated host identifier objects that can also have aliases and can include a port. And policy domains have URL prefixes that are the base for the relative URLs in the policies that are contained in the policy domain object. And policies themselves have separate fields for relative URL, query string, and query parameter of which all, some, or just one can be specified. And there is a rich set of meta character patterns as well. Furthermore, a policy has an authentication scheme of which we only used two types; one that required authentication and one that didn't, an anonymous one which was the root of my first big mistake in the plan for migrating our policies. All of these policy aspects combine to define the URL space that a policy domain object and its policies protect and the authentication state that is required.

When finding a policy for a given request URL, OAM first looks for policy domains that match the host and then looks for the most specific of these policy domains by taking the one having the longest prefix that matches the request's URL At that point only URLs of policies within that policy domain container will be consulted for one that matches. And OAM allows you to order those policies. And the first policy to match the request's URL is the only policy that will then be used to either allow or deny access.

With that background lets now look at the corresponding parts of OpenAM's model and how my understanding of OAM's model initially tripped me up on my first approach to migrating our policies  into OpenAM. Hopefully that can help someone avoid the same mistakes.

The OpenAM 12.X Model 

Again to be clear, I am discussing only OpenAM version 12.X and believe that there are no changes to the policy model in version 13.X save for moving from multiple resource URLs per policy to a single resource URL per policy. So for migration we chose to have a single resource URL per policy in OpenAM so that we would have no issues when updating to 13.

OpenAM uses application objects as containers of policies. These have base URLs to which policy resource URLs must be relative. And where OAM has three fields that combine for the policy's URL space OpenAM only provides the resource URL. And OAM implicitly applies policies to requests with and without queries subject to any query string and query parameters if specified while OpenAM must have two separate policies for the exact same URL if we want both the query-less request and the same request having query strings to make it through. With this requirement alone we anticipate that our number of policies will double at a minimum. But I'll outline some mitigation mechanisms in a follow-up post.

Oh, and by the way, if you are using the very cool rest endpoints in OpenAM to manage your policies you'll find that resource URLs are not relative. They are absolute. So the "relative" nature may solely be a UI enforced feature. Have I tried setting a policy's resource URL to something that isn't relative to its containing application object? I have not. Our tool for migrating just honors that contract.

And finally, before discussing finding a policy in OpenAM for a given request URL I need to explain a minor aspect of OpenAM's Web Agents. They can only point to a single OpenAM application object. Since we have a single cluster of reverse proxy agents that share identical configuration via an agent group this means that all of our policies must be contained in a single OpenAM application object. That is very important.

Finding a policy for a given request URL first looks for all policies in the application object pointed to by the web agent which happens to be all of them in our case so no narrowing there. I'm hoping we can change that at some point in a straightforward, backwards compatible way as I'll explain later. Next, it finds all policies whose resource URLs match the request's URL. All of them. Not just the first since OpenAM doesn't support ordering of policies. And access is only allowed if all policies allow the request. But it turns out that statement isn't accurate enough and this is where I made a second huge mistake.

Policy Matching

In OAM the identified policy either allows access or denies access based upon its authorization expression that I won't go into. Actually, the result of evaluating the expression can be either allow, deny, or inconclusive and can each be handled differently. But for us, inconclusive meant deny and we treated it as such.

Things are different in OpenAM. I assumed that once the set of policies were identified that matched the request's URL that access was allowed or denied based upon two additional aspects of OpenAM policies that seemed to be correlated to the authorization expression concept in OAM; namely subjects and conditions.

Subjects that are supported natively include Authenticated Users, Never Match (what good is that???), users and groups, jwt claims, and logical combinators NOT, AND, and OR. And that brings me to my first big mistake. Authenticated Users mapped nicely to OAM's login scheme mechanism. But where was anonymous access. Then I found in one piece of documentation that a NOT wrapping a Never Match would cause the policy to always match which I assumed was my elusive anonymous access. But it wasn't.

In OpenAM's policy engine there is no concept of an unauthenticated user. You only get to policy evaluation if you are already authenticated even if it is with what is known as the "anonymous" module that doesn't prompt for any user credentials. The key here is OpenAM's powerful concept of authentication levels and how you map them to mean different things such as 0 is an authenticated but anonymous user while 10 is a user who authenticated with a username and password and 20 is a user who used a 2nd factor mechanism.

That is why the unenforced url list is part of agent configuration and not policy configuration. If a request's URL matches on the unenforced url list then the agent lets it through. If it isn't, the agent redirects to the login page and only after you've authenticated and been redirected back to the original request URL will the agent then confer with the policy engine to find matching policies to apply to the request.

Now for conditions. Conditions add further requirements beyond the subject. A policy doesn't have to have any conditions in which case only the subject is considered. And this brings me to that second mistake. I assumed that if the subject or condition, if included, didn't apply to the current user that the policy denied access. And remember that if one policy denies access then the request is denied.

And this is totally wrong and why this section is titled Policy Matching. The subject and condition aspects are not used to deny and allow access. They are part of the matching to winnow down further the list of policies that apply to the request. If the user doesn't meet the requirements of the subject and condition, if included, that policy is simply dropped from the candidate list of policies to be applied to the request. And that brings us to one final aspect of policies.

Policy Actions

OAM provided a list of Http methods with check boxes that indicated if a policy applied to GET, POST, PUT, etc. OpenAM provides a similar list called actions also with a checkbox for each. But it also provides a pair of radio buttons with one for Allow and one for Deny. Only when the policies have been further winnowed down via subjects and conditions does OpenAM then attempt to allow or deny a request. And it does so by taking the http method of the request and looking to see which of the remaining policies in the list included that action by checking that action's check box. For each of those it then sees if that policy has Allow or Deny selected. If any have Deny selected then the request is denied. If all have Allow selected then the request is allowed.

How am I certain about all of this? I crafted a number of test scenarios with related policies, subjects, conditions, and related users with specific characteristics that would meet or fail those conditions and tested each with the policy evaluation rest endpoint to obtain the conclusive results. I'll cover these in my next post.