Modeling Delegation of Rights in a simplified XACML with Haskell

Author: Frank Siebenlist <franks@mcs.anl.gov>

Table of Contents

1. Introduction

This set of files are used to model administrative and delegation of rights features in a XACML-like like language, implemented in Haskell. XACML-like means that only a subset of the XACML language is mimicked: only those XACML language features are implemented that were deemed necessary to show a clear link with that language while keeping the implementation as simple enough to show the delegation model construct.

The implementation in Haskell has been kept as simple as possible: clarity prevailed over efficiency.

Non Goals

This code is not meant to end up in any production code...

There is also no intention to fully implement XACML in Haskell. Polar did a fantastic job doing that. Note that the purpose of Polar's effort is to model XACML 1.1 in Haskell in order to make observations about the completeness and formal definition of the XACML language. As stated before, this effort is meant to introduce and facilitate discussions about new XACML language features.

Note, however, that I have borrowed and stolen anywhere I could from Polar's "The Formal Semantics of XACML" document. I took the liberty to deviate where I thought a more simple equivalent would suffice. Furthermore, as every good student, I tried to improve on the master, which means that I fully expect to be corrected by that same master... (but that is how the learning process works best ;-)

Haskel and document structure

Although these documents look like text with some code fragment examples, it is actual executable code which is added in a literate style of programming. If the text files associated with each section have a ".lhs" extension, then you should be able to load them directly in a Haskell interpreter. (If this file is viewed as a web page, then the corresponding code file for this section is XacmlDelegationHaskell1.lhs.)

To be more precise, the embedded code is found inside blocks of text that start on the first column with a ">" with an empty line before and after the code fragment, like:

> module XacmlDelegationHaskell1 where
> import Prelude
> import IO

which essentially is the preamble needed for any subsequent "real" code.

First XACML-like code

To get you warmed up, let's start with modeling the XACML program execution in its most simplistic form:

> type PDP = RequestContextT -> ResponseContextT

which is a function type definition for Policy Decision Point (PDP) functions. It states that these PDP function take a request context as input and return a response context. For our first version of a PDP, we simplify the request context to be identical to a string that identifies the requester:

> type RequestContextT = String

while the response is a simple boolean:

> type ResponseContextT = Bool

As a simple PDP instance we can now write:

> aPdp :: PDP
> aPdp req = (req == "john")

where the instance clearly consists of one rule that evaluates to true if the request context equals "john".

As example requests we can define:

> req1 = "john"
> req2 = "mary"

then the associated results will be:

> resp1 = aPdp req1
> resp2 = aPdp req2

Finally a unit test function is defined that prints out the different requests and associated responses:

> unitTest = do
>     putStrLn ("request: '" ++ req1 ++ "' yields PDP-response: '" ++ show resp1 ++ "'")
>     putStrLn ("request: '" ++ req2 ++ "' yields PDP-response: '" ++ show resp2 ++ "'")

If this text file is loaded in a Haskell interpreter, like hugs, then one can run the unit test function to see if all is working as expected. What follows is not code, but a copy out of the hugs interpreter session that shows the interaction and "my" results:

XacmlDelegationHaskell1> unitTest
request: 'john' yields PDP-response: 'True'
request: 'mary' yields PDP-response: 'False'
XacmlDelegationHaskell1> 

I promise that the next sections will be a little more interesting...

2. Putting some structure in the Request Context

In this section, we will raise the sophistication level a little by adding some structure to the request and response contexts.

(If this file is viewed as a web page, then the corresponding code file is XacmlDelegationHaskell2.lhs.)

First we'll add some stuff from the previous section that doesn't need to be discussed:

> module XacmlDelegationHaskell2 where
> import Prelude
> import IO
> type PDP = RequestContextT -> ResponseContextT

We will now put some structure in the request context with subject/resource/action attributes:

> data RequestContextT = 
>     RequestContext {
>            subject  :: SubjectT,
>            resource :: ResourceT,
>             action   :: ActionT
>           } deriving(Show)
> type SubjectT  = String
> type ResourceT = String
> type ActionT   = String

The subject/resource/action attributes are still simple strings, but that will suffice for now.

The response context will also become a data structure with a decision that is still a simple boolean:

> data ResponseContextT = 
>     ResponseContext{
>            decision :: DecisionT
>      } deriving(Show)
> type DecisionT = Bool

As a simple PDP instance with a simple policy rule, we can write:

> aPdp :: PDP
> aPdp req = ResponseContext { decision = ((subject req == "john") && (resource req == "abc")) }

or in words, "john can perform any action on resource abc.

As example requests, we can define:

> req1 = RequestContext { subject="john", resource="abc", action="read" }
> req2 = RequestContext { subject="john", resource="def", action="read" }
> req3 = RequestContext { subject="mary", resource="abc", action="read" }

then the results will be:

> resp1 = aPdp req1
> resp2 = aPdp req2
> resp3 = aPdp req3


> unitTest = do
>     putStrLn ("req1: " ++ show req1)
>     putStrLn ("resp1: " ++ show resp1)
>     putStrLn ("req2: " ++ show req2)
>     putStrLn ("resp2: " ++ show resp2)
>     putStrLn ("req3: " ++ show req3)
>     putStrLn ("resp3: " ++ show resp3)

After evaluating this file, and asking the interpreter to evaluate the unit test, the following showed up on my screen:

XacmlDelegationHaskell2> unitTest
req1: RequestContext{subject="john",resource="abc",action="read"}
resp1: ResponseContext{decision=True}
req2: RequestContext{subject="john",resource="def",action="read"}
resp2: ResponseContext{decision=False}
req3: RequestContext{subject="mary",resource="abc",action="read"}
resp3: ResponseContext{decision=False}
XacmlDelegationHaskell2> 

3. Permit, Deny, & NotApplicable

In this section we move to a Permit/Deny style of authorization decisions instead of the simple boolean result.

(If this file is viewed as a web page, then the corresponding code file is XacmlDelegationHaskell3.lhs.)

The relevant code fragments copied from the previous section are here:

> module XacmlDelegationHaskell3 where
> import Prelude
> import IO
> type PDP = RequestContextT -> ResponseContextT
> data RequestContextT = 
>     RequestContext {
>            subject  :: SubjectT,
>            resource :: ResourceT,
>             action   :: ActionT
>           } deriving(Show)
> type SubjectT  = String
> type ResourceT = String
> type ActionT   = String
> data ResponseContextT = 
>     ResponseContext{
>            decision :: DecisionT
>            } deriving (Show)

We will now define the decision to become more permit/deny-like:

> data DecisionT = Permit
>               | Deny
>               | NotApplicable
>                 deriving (Eq, Show)

which essentially defines a data structure with an choice of different types (or values).

Note that XACML's "Indeterminate" has been discarded for simplicity.

To arrive at permit/deny decision, we have to define the functions that transforms a boolean outcome of a rule evaluation in such a value:

> type DecisionF = Bool -> DecisionT
> permitEffect :: DecisionF
> permitEffect True  = Permit
> permitEffect False = NotApplicable
> denyEffect :: DecisionF
> denyEffect True  = Deny
> denyEffect False = NotApplicable

which is a functional style with pattern matching of defining the two functions "permitEffect" and denyEffect" that transform a boolean in the expected DecisionT value.

Similar to the previous section, we define two simple PDP instances which hold different policy rules:

> aPdp1 :: PDP
> aPdp1 req = ResponseContext { decision = permitEffect ((subject req == "john") && (resource req == "abc")) }

> aPdp2 :: PDP
> aPdp2 req = ResponseContext { decision = denyEffect ((subject req == "john") && (resource req == "def")) }

or in words, "john" can perform any action on resource "abc" according to PDP1, but can not access "def" according to PDP2.

As example requests, we can define:

> req1 = RequestContext { subject="john", resource="abc", action="read" }
> req2 = RequestContext { subject="john", resource="def", action="read" }
> req3 = RequestContext { subject="mary", resource="abc", action="read" }

with a unit test function that shows the result of the different permutations:

> unitTest = do
>     putStrLn (show req1 ++ " =>aPdp1=> " ++ show (aPdp1 req1))
>     putStrLn (show req2 ++ " =>aPdp1=> " ++ show (aPdp1 req2))
>     putStrLn (show req3 ++ " =>aPdp1=> " ++ show (aPdp1 req3))
>     putStrLn (show req1 ++ " =>aPdp2=> " ++ show (aPdp2 req1))
>     putStrLn (show req2 ++ " =>aPdp2=> " ++ show (aPdp2 req2))

On my screen, the evaluation of the unitTest functions yielded:

XacmlDelegationHaskell3> unitTest
RequestContext{subject="john",resource="abc",action="read"} =>aPdp1=> ResponseContext{decision=Permit}
RequestContext{subject="john",resource="def",action="read"} =>aPdp1=> ResponseContext{decision=NotApplicable}
RequestContext{subject="mary",resource="abc",action="read"} =>aPdp1=> ResponseContext{decision=NotApplicable}
RequestContext{subject="john",resource="abc",action="read"} =>aPdp2=> ResponseContext{decision=NotApplicable}
RequestContext{subject="john",resource="def",action="read"} =>aPdp2=> ResponseContext{decision=Deny}
XacmlDelegationHaskell3> 

4. Top Level Decision Combinators

This section will add top level combinator functions that combine the results of multiple policy evaluations.

(If this file is viewed as a web page, then the corresponding code file is XacmlDelegationHaskell4.lhs.)

Note that even though the combinator that is discussed here is very similar to the policy combinator in XACML, its use is defined one level higher. It is the "unspoken" combinator that a PDP uses when it has to resolve multiple decisions from multiple matching policy-sets and policies. In this simplified model, a policy is the equivalent of a policy-set/policy, and this simplified policy doesn't have any nested policies and only has one single rule. The latter restrictions mean that we don't need policy and rule combinators.

Furthermore, there will be no equivalent of XACML's Target. One can imagine all rules/policies/policy-sets having the same target with anySubject/anyResource/anyAction, and that all further applicability of the rule is determined by the rule definition itself.

First, we reuse the following code from the previous section:

> module XacmlDelegationHaskell4 where
> import Prelude
> import IO
> type PDP = RequestContextT -> ResponseContextT
> type SubjectT  = String
> type ResourceT = String
> type ActionT   = String
> data RequestContextT = 
>     RequestContext {
>            subject  :: SubjectT,
>            resource :: ResourceT,
>             action   :: ActionT
>           } deriving(Show)
> data DecisionT = Permit
>               | Deny
>               | Indeterminate
>               | NotApplicable
>                 deriving (Eq, Show)
> data ResponseContextT = 
>     ResponseContext{
>            decision :: DecisionT
>            } deriving (Show)
> type DecisionF = Bool -> DecisionT
> permitEffect :: DecisionF
> permitEffect True  = Permit
> permitEffect False = NotApplicable
> denyEffect :: DecisionF
> denyEffect True  = Deny
> denyEffect False = NotApplicable

The next step is seeding the PDP with multiple policies and use a combinator to render the final authorization decision. First we will define a rule "type" as a function:

> type RuleF = RequestContextT -> DecisionT
> type PolicyT = RuleF

which states that this RuleF function type takes a request context and returns a permit/deny kind of decision. We subsequently define a policy type as the equivalent of such a rule type to keep it closer to our XACML mindset. So again, a policy here only has one single rule without any targets, and XACML's policy-sets and policies are the equivalent of this PolicyT.

Some policy examples are:

> johnPolicy1 :: PolicyT
> johnPolicy1 req = permitEffect ( (subject req) == "john" )
> maryPolicy1 :: PolicyT
> maryPolicy1 req = denyEffect ( (subject req) == "mary" )
> maryPolicy2 :: PolicyT
> maryPolicy2 req = permitEffect ( (subject req) == "mary" )

Then we define a data type for a list of these policies as:

> type PoliciesT = [PolicyT]

and we stuff all the previous example policies in such a list:

> examplePolicies :: PoliciesT
> examplePolicies = [johnPolicy1, maryPolicy1, maryPolicy2]

To render a decision over multiple decisions, we rely on a binary combinator that takes two decisions and combines them into one:

> type BinaryDecisionCombinatorF = DecisionT -> DecisionT -> DecisionT

and we can define a deny-override binary combinator function as:

> binaryDenyOverrides :: BinaryDecisionCombinatorF
>
> binaryDenyOverrides Deny _ = Deny
> binaryDenyOverrides _ Deny = Deny
> binaryDenyOverrides Permit _ = Permit
> binaryDenyOverrides _ Permit = Permit
> binaryDenyOverrides NotApplicable _ = NotApplicable
> binaryDenyOverrides _ NotApplicable = NotApplicable

which makes heavily use of functional pattern matching to define what is a simple mapping table.

If we have a list of decisions, then we can use the following function type and instance definition for the combinator that will yield a single decision:

> type DecisionsCombinatorF = BinaryDecisionCombinatorF -> [DecisionT] -> DecisionT

> theDecisionsCombinator aBinaryCombinator decisions = foldr aBinaryCombinator NotApplicable decisions

which uses this "foldr" list comprehension function to "fold" the "decisions" list into a single decision by applying the "aBinaryCombinator" recursively over pairs of decisions until no more. The "NotApplicable" decision is used as the seed to start the reduction.

We can now "pre-configure" a denyOverridesDecisionsCombinator from the generic theDecisionsCombinator by seeding it with this binaryDenyOverrides combinator:

> type ConfiguredDecisionsCombinatorF = [DecisionT] -> DecisionT
> denyOverridesDecisionsCombinator :: ConfiguredDecisionsCombinatorF
> denyOverridesDecisionsCombinator decisions = theDecisionsCombinator binaryDenyOverrides decisions

Going back to our original PDP data type, we can define a more generic PDP "ancestor" that can be pre-configured with the decision combinator of choice and the list of policies that should apply:

> type GenericPDP = RequestContextT -> ConfiguredDecisionsCombinatorF -> PoliciesT -> ResponseContextT
> theGenericPDP :: GenericPDP
> theGenericPDP request combinator policies = ResponseContext{ 
>                                                    decision = (combinator (map (\policy -> policy request) policies))}

which allows us now to create a PDP instance which is pre-configured with a "denyOverridesDecisionsCombinator" and our list of "examplePolicies":

> ourExamplePDP :: PDP
> ourExamplePDP request = theGenericPDP request denyOverridesDecisionsCombinator examplePolicies

then we invent some example requests:

> johnRequest = RequestContext { subject = "john", resource = "abc", action = "read" }
> maryRequest = RequestContext { subject = "mary", resource = "abc", action = "read" }
> jimRequest =  RequestContext { subject = "jim",  resource = "abc", action = "read" }

and create a unit test function to see if we get what we were expecting:

> unitTest = do
>     putStrLn (show johnRequest ++ " => PDP => " ++ show (ourExamplePDP johnRequest))
>     putStrLn (show maryRequest ++ " => PDP => " ++ show (ourExamplePDP maryRequest))
>     putStrLn (show jimRequest ++ "  => PDP => " ++ show (ourExamplePDP jimRequest))

After evaluating the above, and asking the interpreter the following:

XacmlDelegationHaskell4> unitTest
RequestContext{subject="john",resource="abc",action="read"} => PDP => ResponseContext{decision=Permit}
RequestContext{subject="mary",resource="abc",action="read"} => PDP => ResponseContext{decision=Deny}
RequestContext{subject="jim",resource="abc",action="read"}  => PDP => ResponseContext{decision=NotApplicable}
XacmlDelegationHaskell4> 

To recap this section, we are now able to create a PDP instance by pre-configuring it with an overall combinator and a list of policies that apply. The next section will dive into the issues concerning delegation if we start associating an "issuer" with a policy.

5. Delegation Chaining through Policy Issuers

This section will discuss how the delegation of access rights could be modeled by associating an issuer with a policy.

(If this file is viewed as a web page, then the corresponding code file is XacmlDelegationHaskell5.lhs.)

The most important premise is that a policy is always associated with an issuer.,This issuer is someone who states that the adherence of that policy matters to him or her. For example, if john "says" that mary is allowed read access of file abc, then it is a policy that he states and nobody else. Whether jim, alice, you or me care what john says about mary's rights, has to be expressed in other policy statements about john, made by jim, alice, you or me.

There are different solutions of how to go about it in the literature and in existing implementations. One of the most simple models is that if alice give john read access to file abc, she implicitly also allows him to delegate those rights to others, and alice will honor those delegated rights. So, if alice gives john read access right to files abc, and john gives mary read access rights to files abc, then alice will also give mary read access rights to file abc. The latter is true even if alice has never seen mary before, because she "trusts" john to "do the right thing".

This simple delegation of rights scheme will first be modeled in this section. A slightly more sophisticated model where one differentiates between the rights to access and the rights to administer access will be dealt with in a next section.

As usual, we start by copying from the previous section what we can reuse here:

> module XacmlDelegationHaskell5 where
> import Prelude
> import IO
> type SubjectT = String
> type ResourceT = String
> type ActionT = String
> type PDP = RequestContextT -> ResponseContextT
> data RequestContextT = 
>     RequestContext {
>            subject  :: SubjectT,
>            resource :: ResourceT,
>             action   :: ActionT
>           } deriving (Show)
> data DecisionT = Permit
>               | Deny
>               | NotApplicable
>                 deriving (Eq, Show)
> data ResponseContextT = 
>     ResponseContext{
>            decision :: DecisionT
>            } deriving (Show)
> type DecisionF = Bool -> DecisionT
> permitEffect :: DecisionF
> permitEffect True = Permit
> permitEffect False = NotApplicable
> denyEffect :: DecisionF
> denyEffect True = Deny
> denyEffect False = NotApplicable
> type RuleF = RequestContextT -> DecisionT
> type PoliciesT = [PolicyT]

We will start out by associating a policy with an issuer. This means that someone has stated some access control policy that should be applied, and it's up to the PDP to find out whether it should care about it. In other words, there should be additional policy for the PDP such that it can determine how different policy statements issued by different issuers should be weighted and taken into account to render a single authorization decision.

So in the most simple case, we redefine PolicyT as:

> type IssuerT = SubjectT
> data PolicyT = Policy {
>               policyRule   :: RuleF,
>               policyIssuer :: IssuerT
>               }

Note that the policy issuer has the same data type as the subject in a request context. The reason for this will become clear later on.

This PolicyT definition allows us to write a policy instance like:

> johnPolicy1Pete = Policy{policyRule = (\r -> permitEffect ((subject r) == "john")), policyIssuer = "pete"}

which tells us that "pete" states that he allows subject "john" to access essentially any resource. Whether the PDP cares what pete allows john to do is a separate issue, and subject to policy that will be further explored.

In general, however, a PDP can evaluate all policy statements issued by any issuer, and each of these evaluations renders a decision based on the request context and that particular policy statement.

In other words, each individual policy statement renders a decision for a request context, and as each policy has an issuer associated with it, this decision is a decision associated with that same issuer.

When the PDP evaluates an access request then each policy statement is evaluated within that context and renders an individual decision, which combined with the decisions of all other policy statements will render the ultimate authorization decision.

We can translate that observation by redefining a decision as having a issuer as well as the evaluation result.

Here we introduce the AznDecisionT, which is a decision structure that includes the issuer of that decision, i.e. according to who's policy the decision was rendered:

> data AznDecisionT = AznDecision {
>                         aznRequestContext     :: RequestContextT,
>                         aznDecision           :: DecisionT,
>                         aznDecisionIssuer     :: IssuerT,
>                         aznDelegatorDecisions :: [AznDecisionT]
>                         } deriving(Show)

For future convenience, the decision structure has been extended to include the request context on which this decision was based, and a list of decisions by delegators that pertain to the issuer. The latter will be discussed in more detail further down.

For the discussion, we will fill-in some policy statements that will be used as an example:

> maryPolicy2Pete = Policy{policyRule=(\r -> denyEffect   ((subject r) == "mary")),  policyIssuer = "pete"}
> petePolicy1Dave = Policy{policyRule=(\r -> permitEffect ((subject r) == "pete")),  policyIssuer = "dave"}
> davePolicy1Pdp  = Policy{policyRule=(\r -> permitEffect ((subject r) == "dave")),  policyIssuer = "PDP"}
> lauraPolicy2Pdp = Policy{policyRule=(\r -> permitEffect ((subject r) == "laura")), policyIssuer = "PDP"}
> johnPolicy1Ben  = Policy{policyRule=(\r -> permitEffect ((subject r) == "john")),  policyIssuer = "ben"}

> examplePolicies :: PoliciesT
> examplePolicies = [johnPolicy1Pete,maryPolicy2Pete,petePolicy1Dave,davePolicy1Pdp,lauraPolicy2Pdp,johnPolicy1Ben]

If we also consider the following request context:

> johnsRequest = RequestContext { subject = "john", resource = "abc", action = "read" }

then we could evaluate an authorization decision for each of these policy statements.

Doing this "by hand", and using a slightly more readable notation, this would yield for "johnPolicy1Pete" a decision "<john|P|pete>", which ignores resources and actions and depicts that a subject "john" has been given a Permit result "P" according to policy issued by "pete". We could also have a "N" for NotApplicable or an "D" for Deny in the result field.

With this notation, the six policy statements would yield: <john|P|pete>,<john|N|pete>,<john|N|dave>,<john|N|PDP>,<john|N|PDP>,<john|P|ben> which show that only pete and ben have issued policy statements that directly pertain to john, while the other four statements yield a NotApplicable.

We now need an evaluator that takes a request context and a single policy, and return the azn decision for that single policy:

> policyEvaluator :: RequestContextT -> PolicyT -> AznDecisionT
> policyEvaluator aRequest aPolicy = AznDecision {
>                               aznDecision = (policyRule aPolicy) aRequest,
>                               aznRequestContext = aRequest,
>                               aznDecisionIssuer = policyIssuer aPolicy,
>                               aznDelegatorDecisions = []                      
>                               }

As an example, we can evaluate johnsRequest according to pete's policy:

> petesDecisionOnJohn = policyEvaluator johnsRequest johnPolicy1Pete

which yields:

XacmlDelegationHaskell5> petesDecisionOnJohn
AznDecision{aznRequestContext=RequestContext{subject="john",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="pete",aznDelegatorDecisions=[]}

or making that work on the list of policies and filtering out the NotApplicable:

> preliminaryDecisionsOnExamplePoliciesOnJohn = filter (\d -> (aznDecision d) /= NotApplicable ) 
>                                              (map (\p -> policyEvaluator johnsRequest p) examplePolicies)

which yields:

XacmlDelegationHaskell5> preliminaryDecisionsOnExamplePoliciesOnJohn
[AznDecision{aznRequestContext=RequestContext{subject="john",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="pete",aznDelegatorDecisions=[]},AznDecision{aznRequestContext=RequestContext{subject="john",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="ben",aznDelegatorDecisions=[]}]
XacmlDelegationHaskell5> 

As shown, the result is obtained by filtering out all the NotApplicable decisions from the list of evaluation results of john's request over the list of all policies. We do not yet know how to combine the different explicit policy decisions, but we do know that we can ignore all those NotApplicable decisions.

So, this preliminary result is a list of all the explicit decisions that have been rendered according to the issuers of those respective policies: <john|P|pete> and <john|P|ben>.

If Access Right constitutes Delegation Right

At this point, we have to make a choice of how we will interpret the different policy statements and their associated decisions made by the different issuers. The first option that we will explore is the policy where anyone who is granted access right to invoke a certain action on a certain resource is allowed to delegate those rights to someone else.

So, if for example "dave" states that "pete" is allowed to "read" "abc", then "dave" implicitly allows "pete" to give those same access right to "john".

For this delegation scheme, without loss of generality, we could now construct a dependency graph between all these different authorization decisions. In order to get all the relevant authorization decisions, we have to take the initial list of decisions that we found for the original request context, and recursively render the decisions for the issuers of those decisions, meaning, we have to substitute the subject in the request context with the issuer.

When we have the previous example decisions: <john|P|pete> and <john|P|ben>, pruned from the NotApplicable results, then we should take the issuers associated with these decisions, and re-render the authorization decisions with those issuers as subjects in the same request context as before. This process should be repeated recursively for each of the new decisions that do not have a "NotApplicable" decision.

The "aznDelegatorDecisions" attribute in the AznDecisionT structure will hold the list of authorization decisions that pertain to the issuer of that particular decision.

Again, we can ignore all the NotApplicable decisions, and only keep the explicit Permit and Deny results.

With our simplified notation depicting the list of delegator's decisions at the end between [..], we would end up with a tree like structure like:

<john|P|pete[<pete|P|dave[dave|P|PDP]]>
<john|P|ben[]>

which shows how the decision for john by pete is chained through dave to PDP, while ben's decision on pete stands on its own.

Note that generating this tree of decision chains is in general impossible to do for authorization policy statements...

The Root Issuer

We now have to introduce more detailed policy rules to combine the different decision results into a single authoritative authorization decision.

First, we have to identity the single issuer that is the trust root for the pdp. This means that this issuer is the ultimate authority, and the ball stops there literally. It also means that the delegation chain has to end with that single issuer, otherwise the chain itself will yield a NotApplicable result.

If we take our example, this implies that for a root issuer of "PDP", the second single statement "chain" <john|P|ben[]>, doesn't convey any authority by itself, and should yield therefor a NotApplicable result. The first chain <john|P|pete[<pete|P|dave[dave|P|PDP]]>, does end with the root issuer PDP, and we can therefor reduce the chain stepwise:

<john|P|pete[<pete|P|dave[dave|P|PDP]]> => <john|P|pete[<pete|P|PDP]]> => <john|P|PDP]]>

working our way up from the bottom, we can reduce the two last decisions in the chain by taking the "middle-man" out of the loop. This is essentially saying that if dave says pete is allowed, and PDP says dave is allowed, then PDP says pete is allowed.

We have to be careful to reduce different decision results. For example, if we have:

<john|P|pete[<pete|P|dave[dave|D|PDP]]>

where PDP denies dave explicitly access, then we should not care what dave's decision on pete's access is, and it should therefor yield NotApplicable:

<john|P|pete[<pete|P|dave[dave|D|PDP]]> => <john|P|pete[<pete|N|PDP]]> => <john|N|PDP]]>

and the reduced chain itself will become a NotApplicable decision.

Combinator of Decisions according to different Issuers

The other explicit policy that we have to consider is the combination of different decisions on the same "level". For example, if we have:

<john|P|pete[
      [<pete|P|david[david|P|PDP]
      [<pete|D|carol[carol|P|PDP]]>

which constitutes two delegation chains that end with the root issuer PDP, and show different access decision for pete's access according to david's permit and carol's deny. In this case, we have to rely on a higher level policy, like DenyOverrides or PermitOverrides, to resolve the associated ambiguity. In the case of a DenyOverrides policy, carol's decision on pete prevails, which doesn't allow pete to render access decisions, and the delegation chain will reduce to a NotApplicable decision. In the PermitOverrides case, david's decision takes precedence, and the chain will reduce to a Permit decision issued by PDP.

There are a number of other ambiguities to deal with in the next section, like circular dependencies and delegation depth.

Furthermore, there is also the option not to associate the right to access directly with the right to delegate by having separate policy statements for the access rights and delegation rights. This will also be dealt with in following sections.

Model the above in code

Given a request context and a set of policies, "evalPolicies" returns a list of decisions pruned from those NotApplicable:

> evalPolicies :: RequestContextT -> [PolicyT] -> [AznDecisionT]
> evalPolicies _ [] = []
> evalPolicies req ps = filter (\d -> (aznDecision d) /= NotApplicable)  (map (\p -> (policyEvaluator req p)) ps)

To substitute the request context's subject with the issuer of the policy/decision, we define a helper function "substituteIssuer" that takes a decision, uses its request context to generate a new, cloned request context that uses the decision's issuer as the subject:

> type SubstituteIssuerF = AznDecisionT -> RequestContextT
> substituteIssuer aDecision = (aznRequestContext aDecision) { subject = aznDecisionIssuer aDecision }

Given an initial decision, we have to determine the delegator's decisions that apply to that decision. "evalDelegatorsDecision" takes a decision and a list of policies to apply, and recursively fills in the list of delegator's decisions in the decision's "aznDelegatorDecisions" slot. No combinator or reduction policies are applied yet, except for the pruning of NotApplicable decisions:

> evalDelegatorsDecision :: AznDecisionT -> [PolicyT] -> AznDecisionT
> evalDelegatorsDecision aAznDecision [] = aAznDecision
> evalDelegatorsDecision aAznDecision _ | (aznDecision aAznDecision == NotApplicable) = aAznDecision
> evalDelegatorsDecision aAznDecision ps = 
>     aAznDecision {aznDelegatorDecisions = filter (\d -> (aznDecision d) /= NotApplicable) 
>                                                 (map (\dd -> evalDelegatorsDecision dd ps) 
>                                                      (evalPolicies (substituteIssuer aAznDecision) ps))
>                 }

"evalDelegatorsDecision" essentially builds the whole delegation decision tree. To test it out, we can define and evaluate the following:

> initialDecisionsForJohn = filter (\d -> (aznDecision d) /= NotApplicable) (evalPolicies johnsRequest examplePolicies)
> decisionsWithDelegatorsForJohn = map (\dd -> evalDelegatorsDecision dd examplePolicies) initialDecisionsForJohn 

which yields:

XacmlDelegationHaskell5> initialDecisionsForJohn
[AznDecision{aznRequestContext=RequestContext{subject="john",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="pete",aznDelegatorDecisions=[]},AznDecision{aznRequestContext=RequestContext{subject="john",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="ben",aznDelegatorDecisions=[]}]
XacmlDelegationHaskell5> decisionsWithDelegatorsForJohn
[AznDecision{aznRequestContext=RequestContext{subject="john",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="pete",aznDelegatorDecisions=[AznDecision{aznRequestContext=RequestContext{subject="pete",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="dave",aznDelegatorDecisions=[AznDecision{aznRequestContext=RequestContext{subject="dave",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="PDP",aznDelegatorDecisions=[]}]}]},AznDecision{aznRequestContext=RequestContext{subject="john",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="ben",aznDelegatorDecisions=[]}]
XacmlDelegationHaskell5> 

Now we need combinator and reduction functions that with a request context works on this list of decisions and comes up with a single decision. We need combinators that reduce a list of decisions to a single decision, and a "reductor" that fold a delegation decision chain.

We start with creating a combinator that uses a deny-overrides policy.

The "aznDecisionDenyOverrides" binary function describes the mapping table to use to combine two authorization decision with a DenyOverrides policy:

> aznDecisionDenyOverrides :: AznDecisionT -> AznDecisionT -> AznDecisionT
> aznDecisionDenyOverrides a1 _ | (aznDecision a1 == Deny) = a1
> aznDecisionDenyOverrides _ a2 | (aznDecision a2 == Deny) = a2
> aznDecisionDenyOverrides a1 _ | (aznDecision a1 == Permit) = a1
> aznDecisionDenyOverrides _ a2 | (aznDecision a2 == Permit) = a2
> aznDecisionDenyOverrides a1 _ | (aznDecision a1 == NotApplicable) = a1
> aznDecisionDenyOverrides _ a2 | (aznDecision a2 == NotApplicable) = a2

Then we define a "AznDecisionCombinatorF" function type that takes a list of decisions and combines them into one. It also takes a root issuer as a parameter to pass down the individual decisions to reduce the chain:

> type AznDecisionCombinatorF = IssuerT -> [AznDecisionT] -> AznDecisionT

By using the previous "aznDecisionDenyOverrides" and a given root issuer, we can fold a list of decisions to a single decision with "denyOverridesAznDecisionCombinator":

> denyOverridesAznDecisionCombinator :: AznDecisionCombinatorF
> denyOverridesAznDecisionCombinator aRootIssuer aznDecisions = 
>     foldr 
>     aznDecisionDenyOverrides
>     AznDecision{aznDecision = NotApplicable, aznDecisionIssuer = aRootIssuer}
>           (map 
>                (\a -> (decisionChainReductor denyOverridesAznDecisionCombinator aRootIssuer a)) 
>                aznDecisions)

The generic "decisionChainReductor" function used above, folds over a chain of decisions, by using the supplied decision combinator, e.g. DenyOverride, and a root issuer:

> decisionChainReductor :: AznDecisionCombinatorF -> IssuerT -> AznDecisionT -> AznDecisionT
> decisionChainReductor aAznDecisionCombinator aRootIssuer aDecision | (aRootIssuer == aznDecisionIssuer aDecision) = aDecision
> decisionChainReductor aAznDecisionCombinator aRootIssuer aDecision | (aznDecision aDecision == NotApplicable) = aDecision
> decisionChainReductor aAznDecisionCombinator aRootIssuer aDecision = 
>     aDecision{
>      aznDecision = delegatedDecision, 
>       aznDecisionIssuer = (aznDecisionIssuer delegatorsAznDecision), 
>       aznDelegatorDecisions = (aznDelegatorDecisions delegatorsAznDecision)}
>     where 
>       delegatorsAznDecision = (decisionChainReductor aAznDecisionCombinator aRootIssuer 
>                                                  (aAznDecisionCombinator aRootIssuer (aznDelegatorDecisions aDecision)))
>       delegatedDecision = if (aznDecision delegatorsAznDecision) == Permit then (aznDecision aDecision) else Permit

The "decisionChainReductor" essentially uses the supplied "aAznDecisionCombinator" to combine all the "aznDelegatorDecisions" into a single decision, and then reduces the chain by one if this delegated decision yielded a Permit. This process is started from the bottom which is recursively found by looking for root issuer.

We can test this out again on our example:

> theUltimateResultforJohn = denyOverridesAznDecisionCombinator "PDP" decisionsWithDelegatorsForJohn

which yields:

XacmlDelegationHaskell5> theUltimateResultforJohn
AznDecision{aznRequestContext=RequestContext{subject="john",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="PDP",aznDelegatorDecisions=[]}
XacmlDelegationHaskell5> 

Pre-configuring a PDP

We can define a basePDP as a PDP that we also supply with the top level policies, like the root issuer, the combinators to use, and the set of policies:

> basePDP :: AznDecisionCombinatorF -> IssuerT -> PoliciesT -> RequestContextT -> ResponseContextT
> basePDP c i ps r = ResponseContext{ 
>                           decision = (aznDecision (c i (map (\dd -> evalDelegatorsDecision dd ps) 
>                                            (filter (\d -> (aznDecision d) /= NotApplicable) (evalPolicies r ps)))))
>                          }

We can now "seed" a PDP instance with the DenyOverride combinator, the "PDP" root issuer and the example policies:

> examplePDP :: PDP
> examplePDP aRequestContext = basePDP denyOverridesAznDecisionCombinator "PDP" examplePolicies aRequestContext

and with that examplePDP, we can define some additional requests to see if we get what we expect:

> maryRequest = RequestContext { subject = "mary", resource = "abc", action = "read" }
> jimRequest = RequestContext { subject = "jim", resource = "abc", action = "read" }

and defining an appropriate unit test:

> unitTest = do
>     putStrLn (show johnsRequest ++ " =>examplePDP=> " ++ show (examplePDP johnsRequest))
>     putStrLn (show maryRequest ++ " =>examplePDP=> " ++ show (examplePDP maryRequest))
>     putStrLn (show jimRequest ++ " =>examplePDP=> " ++ show (examplePDP jimRequest))

which yields:

XacmlDelegationHaskell5> unitTest
RequestContext{subject="john",resource="abc",action="read"} =>examplePDP=> ResponseContext{decision=Permit}
RequestContext{subject="mary",resource="abc",action="read"} =>examplePDP=> ResponseContext{decision=Deny}
RequestContext{subject="jim",resource="abc",action="read"} =>examplePDP=> ResponseContext{decision=NotApplicable}
XacmlDelegationHaskell5> 

6. Delegation Policy Choices

This section will discuss how we can give an issuer a choice whether the rights associated with a policy can be delegated, and will define a combinator rule that makes it impossible for a delegate to override decisions made by its delegator.

(If this file is viewed as a web page, then the corresponding code file is XacmlDelegationHaskell6.lhs.)

As usual, we start by copying from the previous section what we can reuse here:

> module XacmlDelegationHaskell6 where
> import Prelude
> import IO
> type SubjectT = String
> type ResourceT = String
> type ActionT = String
> type PDP = RequestContextT -> ResponseContextT
> data RequestContextT = 
>     RequestContext {
>            subject  :: SubjectT,
>            resource :: ResourceT,
>             action   :: ActionT
>           } deriving (Show)
> data DecisionT = Permit
>               | Deny
>               | NotApplicable
>                 deriving (Eq, Show)
> type DecisionF = Bool -> DecisionT
> permitEffect :: DecisionF
> permitEffect True = Permit
> permitEffect False = NotApplicable
> denyEffect :: DecisionF
> denyEffect True = Deny
> denyEffect False = NotApplicable
> type RuleF = RequestContextT -> DecisionT
> type PoliciesT = [PolicyT]
> type IssuerT = SubjectT

Detection and invalidating circular delegation

If john delegates his rights to mary who delegates these same rights to john, then we have an issue of ambiguity through circular reference. We should detect this circularity as we move through the delegation chain, and essentially invalidate that particular chain through a NotApplicable assignment of the associated decision.

Maybe we'll implement this later - leave it as a reader's exercise for now.

Controlling the right to delegate

The issuer of a policy should be able to specify whether the rights that he/she grants to some subject could be delegated onwards to an other subject. There are a number of options implemented and discussed in the literature, and here we indicate this right to delegate with an integer: 0 means no further delegation is allowed, while a positive number indicates the delegation depth it will honor.

We will redefine the policy to reflect this delegation property as:

> data PolicyT = Policy {
>               policyRule         :: RuleF,
>               policyIssuer       :: IssuerT,
>               maxDelegationDepth :: Int
>               }

where the "maxDelegationDepth" indicates the maximum number of delegates it is willing to honor.

This definition allows us to write a policy instance like:

> johnPolicy1Pete  = Policy { policyRule = (\r -> permitEffect ((subject r) == "john")),  policyIssuer = "pete", maxDelegationDepth = 0}
> alicePolicy1Pete = Policy { policyRule = (\r -> permitEffect ((subject r) == "alice")), policyIssuer = "pete", maxDelegationDepth = 1}

which tells us that "pete" states that he allows subject "john" to access essentially any resource, but doesn't allow john to delegates those rights to anyone else. However, "pete" allows "alice" to access any resource and allows her to delegate to rights to someone else, but that someone else is not allowed to delegate those right further on.

In order to keep track of this delegation depth in a potential delegation chain, we have to annotate the associated authorization decision:

> data AznDecisionT = AznDecision {
>                         aznRequestContext           :: RequestContextT,
>                         aznDecision                 :: DecisionT,
>                         aznDecisionIssuer           :: IssuerT,
>                         aznDelegatorDecisions       :: [AznDecisionT],
>                         aznMaxDelegationDepth       :: Int,
>                         aznEffectiveDelegationDepth :: Int
>                         } deriving(Show)

The "aznMaxDelegationDepth" is used to communicate the maximum delegation depth that is allowed beyond the current decision, based on the stated policies that rendered the "aznDelegatorDecisions" as well as the current decision.

The "aznEffectiveDelegationDepth" keeps track of the effective delegation depth with respect to the root issuer.

We will also use this AznDecisionT in the response context as it conveys more information about the evaluation result:

> data ResponseContextT = 
>     ResponseContext{
>            decision :: AznDecisionT
>            } deriving (Show)

To facilitate the discussion, we will fill-in some policy statements that will be used as an example:

> maryPolicy2Pete  = Policy { policyRule = (\r -> denyEffect   ((subject r) == "mary")),  policyIssuer = "pete", maxDelegationDepth = 1}
> petePolicy1Dave  = Policy { policyRule = (\r -> permitEffect ((subject r) == "pete")),  policyIssuer = "dave", maxDelegationDepth = 1}
> davePolicy1Pdp   = Policy { policyRule = (\r -> permitEffect ((subject r) == "dave")),  policyIssuer = "PDP", maxDelegationDepth = 3}
> lauraPolicy2Pdp  = Policy { policyRule = (\r -> permitEffect ((subject r) == "laura")), policyIssuer = "PDP", maxDelegationDepth = 0}
> maryPolicy1Laura  = Policy { policyRule = (\r -> permitEffect ((subject r) == "mary")), policyIssuer = "laura", maxDelegationDepth = 3}

> examplePolicies :: PoliciesT
> examplePolicies = [johnPolicy1Pete,alicePolicy1Pete,maryPolicy2Pete,petePolicy1Dave,davePolicy1Pdp,lauraPolicy2Pdp,maryPolicy1Laura]

Note that a maximum delegation depth for a "denyEffect" policy and a "Deny" decision doesn't make sense as there are no rights to delegate because they are explicitly denied. Neither does a "NotApplicable" decision have anything to contribute to further delegation.

When constructing the delegation chains again, we have a number of cases that should be considered. We will use the same notation for decisions as before, but augment it with the maximum delegation depth from the associated policy. For example:

<john|P|0|pete>
<alice|P|1|pete>
<mary|D|0|pete>
<pete|P|1|dave>
<dave|P|3|PDP>
<laura|P|3|PDP>

If we also consider the following request context:

> johnsRequest = RequestContext { subject = "john", resource = "abc", action = "read" }

then we could evaluate an authorization decision for each of these policy statements as before and recursively go back through the issuers until no more, while pruning the NotApplicable decisions. This would yield the following three chained decisions:

<john|P|0|pete>
         <pete|P|1|dave>
                  <dave|P|3|PDP>

In words, this tells us that "PDP" has given "dave" the access rights with the ability to delegate those rights and that it will honor two more delegates after that. "Dave" gives the rights and one-level delegation rights to "pete", while "pete" only give access rights to "john".

The effective delegation depth starts counting at the root issuer with zero, while the maximum decision delegation depth keeps track of the maximum effective delegation depth where we would loose the right to further delegate

eff=2, max=2: <john|P|0|pete>
eff=1, max=2:          <pete|P|1|dave>
eff=0, max=3:                   <dave|P|3|PDP>

To illustrate what happens when right to further delegate runs out of steam, suppose PDP only gave one level of delegation rights to dave:

eff=2, max=1: <john|P|0|pete>
eff=1, max=1:          <pete|P|1|dave>
eff=0, max=1:                   <dave|P|1|PDP>

One can see that at an effective delegation depth of 2, the decision that pete allows john access becomes NotApplicable because the chain from the root issuer PDP can not be extended beyond an effective depth of 1. Note that this maximum decision delegation depth has a maximum value set by the root issuer and can only be decreased by the subsequent delegates, while any decision where the effective depth is higher than the maximum decision delegation depth is NotApplicable.

Delegates cannot override a delegator

Consider the following example:

<john|P|2|PDP>
<dave|P|2|PDP>
<dave|D|2|john>

The trust root issuer PDP gives both john and dave access rights, while john states that dave's rights are denied. We now have the ambiguity to resolve for dave's access right. The previously used deny-overrides policy would imply that dave's access rights will be denied because of john's stated policy.

However, this doesn't feel right that someone who has delegated rights can override decisions made by that same issuer that delegated those same rights. To resolve this sense of "injustice", we can define a combinator that gives precedence to any explicit decision made with a lower effective delegation depth.

When we add the effective delegation depth to the last example:

eff=0: <dave|P|2|PDP>
eff=1: <dave|D|2|john
eff=0:         [<john|P|2|PDP>]

we see that when the two decisions have to be combined to resolve the access decision for dave, the effective depth of the PDP's decision is lower than the one from john, and therefore has precedence over john's.

Note that the original combinator policy, like deny-overrides or permit-overrides, is still in effect to resolve multiple decisions with the same effective delegation depth.

Modeling the above in code

We start with the "policyEvaluator" from the previous section, where we fill-in the delegation depth attribute values that came from the evaluated policy:

> policyEvaluator :: RequestContextT -> PolicyT -> AznDecisionT
> policyEvaluator aRequest aPolicy = AznDecision {
>                               aznDecision = (policyRule aPolicy) aRequest,
>                               aznRequestContext = aRequest,
>                               aznDecisionIssuer = policyIssuer aPolicy,
>                               aznDelegatorDecisions = [],
>                               aznMaxDelegationDepth = maxDelegationDepth aPolicy,
>                               aznEffectiveDelegationDepth = 0
>                               }

We keep the "evalPolicies" function that essentially iterates with the request over all the policies to yield a pruned list of decisions:

> evalPolicies :: RequestContextT -> [PolicyT] -> [AznDecisionT]
> evalPolicies _ [] = []
> evalPolicies req ps = filter (\d -> (aznDecision d) /= NotApplicable)  (map (\p -> (policyEvaluator req p)) ps)

We still need this "SubstituteIssuerF" helper function:

> type SubstituteIssuerF = AznDecisionT -> RequestContextT
> substituteIssuer aDecision = (aznRequestContext aDecision) { subject = aznDecisionIssuer aDecision }

The "evalDelegatorsDecision" builds up the delegation chains:

> evalDelegatorsDecision :: AznDecisionT -> [PolicyT] -> AznDecisionT
> evalDelegatorsDecision aAznDecision [] = aAznDecision
> evalDelegatorsDecision aAznDecision _ | (aznDecision aAznDecision == NotApplicable) = aAznDecision
> evalDelegatorsDecision aAznDecision ps = aAznDecision {
>                                     aznDelegatorDecisions = 
>                                         filter (\d -> (aznDecision d) /= NotApplicable) 
>                                             (map (\dd -> evalDelegatorsDecision dd ps) 
>                                                  (evalPolicies (substituteIssuer aAznDecision)
>                                                  ps))
>                                    }

For the new "aznDecisionDenyDepthOverrides" function, we take the delegation depth into account, which means that any explicit decision of a lower delegation depth overrides, while for equal depth we can still use the previously defined "aznDecisionDenyOverrides"

> aznDecisionDenyDepthOverrides :: AznDecisionT -> AznDecisionT -> AznDecisionT
> aznDecisionDenyDepthOverrides a1 a2 | (aznEffectiveDelegationDepth a1 == aznEffectiveDelegationDepth a2) = 
>                                aznDecisionDenyOverrides a1 a2
> aznDecisionDenyDepthOverrides a1 a2 | (aznEffectiveDelegationDepth a1 < aznEffectiveDelegationDepth a2) =
>                                if ((aznDecision a1 /= NotApplicable) || (aznDecision a2 == NotApplicable))
>                                then a1  else a2
> aznDecisionDenyDepthOverrides a1 a2 | (aznEffectiveDelegationDepth a1 > aznEffectiveDelegationDepth a2) =
>                                if ((aznDecision a2 /= NotApplicable) || (aznDecision a1 == NotApplicable))
>                                then a2  else a1
>
> aznDecisionDenyOverrides :: AznDecisionT -> AznDecisionT -> AznDecisionT
> aznDecisionDenyOverrides a1 _ | (aznDecision a1 == Deny) = a1
> aznDecisionDenyOverrides _ a2 | (aznDecision a2 == Deny) = a2
> aznDecisionDenyOverrides a1 _ | (aznDecision a1 == Permit) = a1
> aznDecisionDenyOverrides _ a2 | (aznDecision a2 == Permit) = a2
> aznDecisionDenyOverrides a1 _ | (aznDecision a1 == NotApplicable) = a1
> aznDecisionDenyOverrides _ a2 | (aznDecision a2 == NotApplicable) = a2

The "AznDecisionCombinatorF" is changed by using the "aznDecisionDenyDepthOverrides" to fold the decision list:

> type AznDecisionCombinatorF = RequestContextT -> IssuerT -> [AznDecisionT] -> AznDecisionT
> denyOverridesAznDecisionCombinator :: AznDecisionCombinatorF
> denyOverridesAznDecisionCombinator aReq aRootIssuer aznDecisions = 
>     foldr 
>         aznDecisionDenyDepthOverrides
>         AznDecision{aznRequestContext=aReq,aznDecision=NotApplicable,aznDecisionIssuer=aRootIssuer, 
>            aznDelegatorDecisions=[],aznMaxDelegationDepth=0,aznEffectiveDelegationDepth=0}
>         (map 
>             (\a -> (decisionChainReductor denyOverridesAznDecisionCombinator aRootIssuer a)) 
>             aznDecisions)

Finally, the "decisionChainReductor" function becomes somewhat more complicated to incorporate this delegation depth as it has to keep track of the allowed delegation depth of the delegators:

> decisionChainReductor :: AznDecisionCombinatorF -> IssuerT -> AznDecisionT -> AznDecisionT
> decisionChainReductor aAznDecisionCombinator aRootIssuer aDecision | (aRootIssuer == aznDecisionIssuer aDecision) = aDecision
> decisionChainReductor aAznDecisionCombinator aRootIssuer aDecision | (aznDecision aDecision == NotApplicable) = aDecision
> decisionChainReductor aAznDecisionCombinator aRootIssuer aDecision = 
>     aDecision{
>      aznDecision = delegatedDecision, 
>       aznDecisionIssuer = (aznDecisionIssuer delegatorsAznDecision), 
>       aznDelegatorDecisions = (aznDelegatorDecisions delegatorsAznDecision),
>       aznMaxDelegationDepth = maxDepth,
>       aznEffectiveDelegationDepth = effectiveDepth }
>     where 
>       delegatorsAznDecision = (decisionChainReductor aAznDecisionCombinator aRootIssuer 
>                                                    (aAznDecisionCombinator (aznRequestContext aDecision) aRootIssuer (aznDelegatorDecisions aDecision)))
>      effectiveDepth =  aznEffectiveDelegationDepth delegatorsAznDecision + 1
>      maxDepth =  if ((aznMaxDelegationDepth delegatorsAznDecision) - effectiveDepth) < (aznMaxDelegationDepth aDecision)
>                  then (aznMaxDelegationDepth delegatorsAznDecision) 
>                  else ((aznMaxDelegationDepth aDecision) + effectiveDepth)
>       delegatedDecision = if (effectiveDepth > maxDepth) then NotApplicable
>                          else if ((aznDecision delegatorsAznDecision) == Permit) then (aznDecision aDecision) else Permit

When a root issuer is found, the "aznEffectiveDelegationDepth" is started from there and incremented as we go back up the chain. The maximum allowed delegation depth is maintained through the combinator function. If the number of allowed delegates left through the delegators is less than the decision's, then the decision's maximum depth becomes the delegator's, otherwise we recalculate the maximum depth with respect to the effective depth. When we encounter that the maximum allowed delegation depth is larger than the effective one, then we make the decision NotApplicable as our delegated rights ran out of steam.

Testing

For the testing, we reuse the basePDP definition:

> basePDP :: AznDecisionCombinatorF -> IssuerT -> PoliciesT -> RequestContextT -> ResponseContextT
> basePDP aComb aRootIssuer ps req = ResponseContext{ 
>                           decision = (aComb req aRootIssuer (map (\dd -> evalDelegatorsDecision dd ps) 
>                                            (filter (\d -> (aznDecision d) /= NotApplicable) (evalPolicies req ps))))
>                          }

We can now "seed" a PDP instance with the DenyOverride combinator, the "PDP" root issuer and the example policies:

> examplePDP :: PDP
> examplePDP aRequestContext = basePDP denyOverridesAznDecisionCombinator "PDP" examplePolicies aRequestContext

and with that examplePDP, we can define some additional requests to see if we get what we expect:

> maryRequest = RequestContext { subject = "mary", resource = "abc", action = "read" }
> jimRequest =  RequestContext { subject = "jim", resource = "abc", action = "read" }
> daveRequest =  RequestContext { subject = "dave", resource = "abc", action = "read" }
> peteRequest =  RequestContext { subject = "pete", resource = "abc", action = "read" }

and defining an appropriate unit test:

> unitTest = do
>     putStrLn (show (examplePDP johnsRequest))
>     putStrLn (show (examplePDP maryRequest))
>     putStrLn (show (examplePDP jimRequest))
>     putStrLn (show (examplePDP daveRequest))
>     putStrLn (show (examplePDP peteRequest))

which yields:

XacmlDelegationHaskell6> unitTest
ResponseContext{decision=AznDecision{aznRequestContext=RequestContext{subject="john",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="PDP",aznDelegatorDecisions=[],aznMaxDelegationDepth=2,aznEffectiveDelegationDepth=2}}
ResponseContext{decision=AznDecision{aznRequestContext=RequestContext{subject="mary",resource="abc",action="read"},aznDecision=Deny,aznDecisionIssuer="PDP",aznDelegatorDecisions=[],aznMaxDelegationDepth=2,aznEffectiveDelegationDepth=2}}
ResponseContext{decision=AznDecision{aznRequestContext=RequestContext{subject="jim",resource="abc",action="read"},aznDecision=NotApplicable,aznDecisionIssuer="PDP",aznDelegatorDecisions=[],aznMaxDelegationDepth=0,aznEffectiveDelegationDepth=0}}
ResponseContext{decision=AznDecision{aznRequestContext=RequestContext{subject="dave",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="PDP",aznDelegatorDecisions=[],aznMaxDelegationDepth=3,aznEffectiveDelegationDepth=0}}
ResponseContext{decision=AznDecision{aznRequestContext=RequestContext{subject="pete",resource="abc",action="read"},aznDecision=Permit,aznDecisionIssuer="PDP",aznDelegatorDecisions=[],aznMaxDelegationDepth=2,aznEffectiveDelegationDepth=1}}
XacmlDelegationHaskell6> 

Note that all the responses have been reduced to decisions by the root issuer "PDP", even the NotApplicable, that no "aznDelegatorDecisions" are left. Furthermore, the "aznEffectiveDelegationDepth" shows the number of delegates that were used in the decision chain, and the difference between aznMaxDelegationDepth and aznEffectiveDelegationDepth show the number of potential delegates that could have been added according to the policies evaluated.

7. Unification of Policies, PDPs and AznDecisions

.

Note

This section is still very much in draft form - please ignore for now

This section will discuss how a Policy, PDP and AznDecision seem truly equivalent, and that we can push or pull policies, PDPs and AznDecisions within the model. As a consequence, we could also bootstrap PDPs on lightweight servers with a single policy statement such that a central PDP will be consulted for all its authorization decisions.

(If this file is viewed as a web page, then the corresponding code file is XacmlDelegationHaskell7.lhs.)

As usual, we start by copying from the previous section what we can reuse here:

> 

Pulling and pushing of policies

When we look at the way how policies are used in the PDP, there is really no difference if we pre-configure a PDP with a set of policies (through some PAP), or if we supply some policies with the request. As long as the issuer of each policy is clearly identified, there is no ambiguity.

In other words, we could define a new PDP type that would allow one to also supply a set of policies with the request context:

> type PoliciesAcceptingPDP = RequestContextT -> PoliciesT -> ResponseContextT

Pulling and pushing of decisions

It is fairly straightforward to accommodate the use of external PDP by a PDP. In other words, if we associate a PDP with an owner or "issuer", then we could call-out to an external PDP, and associate the returned decision with that issuer. This would allow one to essentially use a call-out to an external PDP in place of any policy.

In similar fashion, an authorization decision by a PDP could also be "pushed" to other PDPs and be considered in the authorization evaluation there. Again, this foreign decision would have an issuer associated with it, which is the foreign PDP's "owner", who takes responsibility for the made authorization decision statement. The receiving PDP will have to verify that the request context that was used by the foreign PDP is identical to the one under consideration, and is so, it can add that decision to the mix of other policies and PDPs.

The Ultimate PDP

See:

> type UltimatePDP = RequestContextT -> PoliciesT -> [UltimatePDP] -> [ResponseContextT] - ResponseContextT

> anUltimatePdp :: UltimatePDP
> anUltimatePdp request policies pdps pdpDecisions = 

After evaluating the above, and asking the interpreter the following:

> unitTest = do
>     putStrLn ("hi")

which yields:

not sure yet...

8. Conclusions & Next Steps

This section gives a short summary of observations that can be made with respect to the presented model. It then briefly highlights some of the required changes in XACML to accommodate a possible implementations of that model.

Summary

In the previous section, a delegation of rights model is presented for an XACML-like language. The simplified language is believed to be close enough to the real XACML language such that the important conclusions from the model do apply to the XACML proper.

What the model proposes is a delegation of rights model based on the notions that:

  • each access policy has an issuer associated with it
  • a policy issuer can indicate whether the permitted rights can be delegated to others or not
  • a policy issuer can specify the maximum number of delegates in a delegation chain that originates from its policy

For a PDP to evaluate an authorization decision based on a request and a set of policies from potentially different issuers, the following PDP-policies have to be defined:

  • a root issuer (or maybe root issuers) have to be identified who are trusted in an absolute sense
  • a policy to combine decisions of different delegation depth
  • a policy to combine decisions that are associated with different issuers

XACML changes

In order to implement the suggested model, there are certain schema and processing changes that have to defined in XACML.

Note that the policies in this write-up are the equivalent of XACML's Policy-set and Policies.

  • Each Policy-set and Policy should include an policy-issuer element that is equivalent to the RequesterContext's Subject element.

    "Equivalent" means that it should be trivial to substitute a requester context' subject information with an arbitrary policy issuer's info.

    (I don't believe that we need categories for the issuer...?)

  • The policy issuer element should be made optional.

    If the policy issuer is not present, it's default value should be the root issuer of that PDP. This provides backwards compatibility with the current XACML 1.1.

  • Each Policy-set and Policy should have an optional delegation depth element or attribute.

    The default value for this delegation depth is 0 (zero), which provides backwards compatibility with XACML 1.1.

  • Each Policy-set and Policy should have optional elements to indicate it's validity time.

    As policies will be pushed or pulled from different sources, we need to associate a life-time with those statements. The checking of this validity time should be integrated with the general evaluation, and should render NotApplicable if the policy statement's validity time doesn't match the environmental context. Note that this validity time is independent from any validity interval as specified in the policy's rule.

The schema and specification changes to XACML 1.1 seem fairly limited, although an actual XACML implementation may have to bear the burden of the added complexity that is in the processing. Note, however, that the additional processing only has to kick in if delegated policies are considered. In other words, the current policy definition and processing model should work equally well in a version that is also capable to deal with delegation.

To do

  • Validate the model and approach within the XACML TC
  • Add/change the present model after TC discussions
  • Detail the schema and processing description changes for the spec
  • Give the approach a formal logical treatment, like Abadi, Lampson, Li's Delegation Logic, (any volunteers to help?)
  • How would this XACML with delegation compare with spki or KeyNote? Any important features missing?