This document is a working document of the SPOCP project, dated 2002-09-05, and it may be updated at any time.
In SPOCP, we are building on both of these works and are in our turn further restricting the syntax. In this document, we describe restricted S-expressions as we are using them.
A restricted S-expression is a nested list enclosed in matching "(" and ")". The first element in the list is always a "tag" or "name" of the object represented by the list and must be a byte-string. With that exception, every element in the list may in turn be an S-expression. In comparison with the S-expression technology of [SEXP], we impose the further restriction that no empty lists are allowed. An S-expression can also be interpreted as an ordered tree: The tag is the root and its first subtree is the second element in the list, and so on. As in SPKI, we have chosen Rivest's compact "canonical form", see [SEXP], as our internal representation of an S-expression. In most of the examples we use the "advanced form" which is easier to read.
SPOCP objects are defined below using ABNF [RFC 2234].
S-expressions are used at the core in the authorization server, and may be sent between computers. If they are, the canonical form is to be used [SEXP]. A canonical S-expression is formed from binary byte strings, each prefixed by its length, and enclosed in parenthesis. The length of a byte string is a non-negative ASCII decimal number, with no unnecessary leading "0" digits, terminated by ":". The canonical form is a unique representation of an S-expression and is used as the input to all hash and signature functions.
s-expr = "(" bytestring *s-part ")"
s-part = bytestring / s-expr / starforms
bytestring = decimal ":" 1*bytes
; The number of bytes should be equal to the decimal specification
decimal = nzdigit *digit
nzdigit = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"
digit = "0" / nzdigit
bytes = %x00-FF
starforms = "(1:*" ( set / range / prefix / any ) ")"
Even though the canonical form is the one described by the ABNF definitions, the so called advanced form will be used in the examples since it is easier for humans to read. In the advanced form, the elements are separated by spaces and there is no length prefix.
Example:
(5:spocp(8:Resource6:mailer)) -- canonical form (spocp (Resource mailer)) -- advanced form
These are two representations of the same S-expression, consisting of a bytestring (the tag) "spocp" and another S-expression, that consists of two bytestrings "Resource" and "mailer".
set = "3:set" 1*s-expr
They are a way of specifying a limited set of elements.
Example:
(* set apple orange lemon)
range = "5:range" rangespec
rangespec = alpha / numeric / date / time / ipv4
alpha = "5:alpha" [lole utf8string [goge utf8string]] / [goge utf8string [lole utf8string]]
numeric = "7:numeric" [ lole number [ goge number ]] / [ goge number [ lole number ]]
date = "4:date" [ goge dat [ lole dat ]] / [ lole dat [ goge dat ]]
time = "4:time" [ lole hms [ goge hms ]] / [ goge hms [ lole hms ]]
ipv4 = "4:ipv4" [ lole ipnum [ goge ipnum ]] / [ goge ipnum [lole ipnum ]]
lole = "1:l" / "2:le"
goge = "1:g" / "2:ge"
number = decimal ":" 1*digit
dat = decimal ":" date
; date format as specified by RFC3339
date-fullyear = 4DIGIT
date-month = 2DIGIT ; 01-12
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
; month/year
time-hour = 2DIGIT ; 00-23
time-minute = 2DIGIT ; 00-59
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
; rules
time-secfrac = "." 1*DIGIT
time-numoffset = ("+" / "-") time-hour ":" time-minute
time-offset = "Z" / time-numoffset
partial-time = time-hour ":" time-minute ":" time-second
[time-secfrac]
full-date = date-fullyear "-" date-month "-" date-mday
full-time = partial-time time-offset
date-time = full-date "T" full-time
hms = decimal ":" partial-time
ipnum = decimal ":" 1*3digit "." 1*3digit "." 1*3digit "." 1*3digit
utf8string = decimal ":" 1*UTF8
UTF8 = %x01-09 / %x0B-0C / %x0E-7F / UTF8-2 /
UTF8-3 / UTF8-4 / UTF8-5 / UTF8-6
UTF8-1 = %x80-BF
UTF8-2 = %xC0-DF UTF8-1
UTF8-3 = %xE0-EF 2UTF8-1
UTF8-4 = %xF0-F7 3UTF8-1
UTF8-5 = %xF8-FB 4UTF8-1
UTF8-6 = %xFC-FD 5UTF8-1
Example
(worktime (* range time ge 08:00:00 le 17:00:00))or
(* range numeric l 15 ge 10)which is the same as
(* set 10 11 12 13 14)
ABNF
prefix = "6:prefix utf8string
Example
(file (* prefix conf))
This expression will match any expression with the tag "file", whose second element is a bytestring that starts with the string "conf".
ABNF
any = "3:any" 1*S-exp
ABNF
extref = decimal ":" ["!"] "urn:spocp:" type ":" typespecific type = 1*lc lc = %x61-7A typespecific = *UTF8
Example
(spocp ... (group urn:spocp:gdbm:groups:eva:member) ...)
Which is a link to a GDBM file and will evaluate to true if the gdbm file 'groups' has a key 'eva' that points to a data item that is equal to 'member'. We will expand on external references in EXTREF.
In order to be able to model any kind of rights as a set of rules in the form of S-expressions, we need to define a binary relation, <=, that can be used to order S-expressions. We want a relation where A<=B belongs to the relation if rule A is less permissive than rule B. For many S-expressions, neither A<=B nor B<=A belongs to the relation.. Once the relation is defined, we also need an efficient way to decide (compute) if A<=B. In SPKI [SPKI], an algorithm to compute "Intersection of tag sets" is specified. Applied to two rules, A and B, it evaluates to the less permissive rule, if A<=B or B<=A. If there is no <=-relation between A and B, it evaluates to the empty S-expression. This type of relation is called a "preorder", and is characterised by being reflexive and transitive, that is by satisfying the two conditions:
Note also that we will leave external references out of the picture for a while.
Trying to define the comparision relation we will start with S-expressions without starform. First some basic definitions:
If S and T are *-free S-expressions, then S <= T if S has at least as many elements as T and every element in S is <= the corresponding element in T.
Example:
(apple (weight 100)(colour red)) is not <= (apple (colour red)(weight 100))
The relationship can also more formally be defined as:
(X_1 X_2 ... X_m) <= (Y_1 Y_2 ... Y_n) if and only if n <= m and X_i <= Y_i for i = 1,...,n
To do the more general comparison you have to go through a number of steps and if anyone of them is true then the comparison is true. If none is true then the comparison is false.
S <= T if:
S = ((* set A B )) and T = (* set (A) (B))
The only existing solution to this is that you the user has to define the allowed S-expressions in your application such that this problem never arises.
For one thing, external references in one S-expression are never compared to anything in another S-expression. They never enter into the comparison rules as descibed above at all. Instead every external reference is expanded on its own and is passed by/ignored in the evaluation of the comparison relation if it is true but will fail the comparison if false.
Building on the descriptions above, with external references the following is true:
Given two lists X = (X_1 X_2 ... X_n) and Y = (Y_1 Y_2 .. Y_n), and given that the number of external references in X is l and in Y k. Then if all the external references evaluates to true, you can when making the comparison between the lists reduce them to lists without the external references, namely X' = (X_1' X_2' ... X_r') and Y' = (Y_1' Y_2' ... Y_s') where r = n-l and s = n-k and then compare them as described above for lists without external references.
When it comes to set comparisions: S <= T if:
Note that external references are not allowed in range or prefix specifications.
Example:
(spocp (resource mailer)(action send)(subject (email eva@minorg.se)))
Which as a rule could be taken to mean that the user that has the emailaddress 'eva@minorg.se' is allowed to use a speified mailserver to send emails to anyone.
A query, on the same topic, could then be:
(spocp (resource mailer)(action send (to roland@dinorg.se))(subject (email eva@minorg.se)))
Example:
If the external reference is:
urn:spocp:extref:uid=${uid},domain="${domain}"
and the query S-expression is
(spocp (resource res)(action some)(subject (uid roland)(domain se catalogix)))
After substitution the external reference will be:
ufn:spocp:extref:uid=roland,domain="se catalogix"
Note that recursive references, like "${foo${bar}}", are not allowed.
type = "flatfile" typespecific = file [":" keyword [":" value *( "," value )]] file = utf8string keyword = utf8string ; keywords are not allowed to start with a '#' se below value = utf8string
Further the format of the flat file has to adher to the following format
line = (data / comment) CR comment = '#' whatever whatever = utf8string data = keyword ":" value *( "," value )
Three cases can appear:
The ABNF of the reference:
type = "time" typespecific = [start] [";" [end] [";" days [";" starttime [";" endtime]]]] start = date ; as specified in 1.2 end = date days = ["0"]["1"]["2"]["3"]["4"]["5"]["6"] ; Sun = 0, Mon = 1, ... starttime = timeofday ; as specified in 1.2 endtime = timeofday
Example:
ufn:spocp:time:2002-08-01_00:00:00;;12345;08:00:00;17:00:00
Which means; starting at 00:00:00 on the 1th of August 2002, every monday, tuesday, wednesday, thursday and friday between 08:00:00 and 17:00:00 inclusive this expression will evaluate to true.
type = "gdbm" typespecific = file [":" keyword [":" value *( "," value )]] file = utf8string keyword = utf8string ; keywords are not allowed to start with a '#' se below value = utf8string
The format of the datum in the GDBM file is supposed to be 'value *("," value)'.
And the semantics are also very similar to flat file, three cases can appear:
ABNF for the LDAP external reference:
type = "ldap"
typespecific = ldapserver ";" thisDN ";" userDN ";" vset
ldapserver = < hostport from Section 5 of RFC1738 [RFC1738] >
thisDN = dn
userDN = dn
dn = < distinguishedName from Section 3 of RFC2253 [RFC2253] >
vset = dnvset / valvset
dnvset = base
/ "(" dnvset ")"
/ "{" dnvset ext attribute "}"
/ dnvset SP conj SP dnvset
/ dnvset ext dnattribute "*"
/ "<" dn ">"
valvset = "[" string "]"
/ "(" valvset ")"
/ dnvset ext attribute
/ valvset SP conj SP valvset
base = "this" / "user" ; refers back to thisDN resp. userDN
conj = "&" / "|"
ext = "/" / "%" / "$" ; base, onelevel resp. subtree search
a = %x41-5A / %x61-7A ; lower and upper case ASCII
d = %x30-39
k = a / d / "-" / ";"
anhstring = 1*k
attribute = a [ anhstring] ; as defined by [RFC2252]
dnattribute = < any attribute name which have attributetype
distinguishedName (1.3.6.1.4.1.1466.115.121.1.12)
like member, owner, roleOccupant, seeAlso, modifiersName, creatorsName,...>
SP = %x20
Example:
<cn=Group,dc=minorg,dc=se>/member
{user$mail & [sven.svensson@minorg.se]}/title & [mib]
<cn=Group,dc=minorg,dc=se>/member & {user%mail & [tvw@minorg.se]}
But what if you'd like to represent everyone that has the same last part P_n. An example of when this would be is if you defined role names within a organization as a concatenation of the organization name, the name of all the organizational units from the top with the roletype. Like this: (role O OU_1 ... OU_n R)
"(role UmU Umdac boss)" would then be the rolename for the boss of the organizational unit Umdac within the organization UmU.
Using this structure you could say (role UmU Umdac) and mean every role within that organizational unit and all the organizational units below. But if you said (role UmU boss ) you would mean the boss of UmU and not all the bosses within UmU. This since (role UmU umdac boss) is not <= (role UmU boss). So adding a role type to a list of O and OU's would mean exactly that role at that level in the organization.
If you instead would define the rolename to be (role R O OU_1 ... OU_n ), you could address every specific roletype within the organization by writing things like (role boss UmU), which would then mean every 'boss' within the organization UmU. Which follows since (role boss UmU OU) is <= (role boss UmU). But you could not specificly target the boss at UmU.
One can add complexity to this by using role types that are hierarchical such that the name would be (role O OU_1 .. OU_n R_1 .. R_m) or (role R_1 .. R_m OU_1 ... OU_n). By using the first form you could address every role within a role hierarchy at a specific place in the organization hierarchy but not in the whole organization tree. Using the later role you could address one whole subtree of the role hierarchy anywhere within a subtree of the organizational hierarchy.
(role UmU admin finance) <= (role UmU admin) (role UmU umdac admin) is not <= (role UmU admin) and (role admin UmU umdac) <= (role admin UmU) (role admin finance UmU) is not <= (role admin UmU)
Remember that the decision of the meaning of a particular rule is taken when modelling the authorisation policy for a particular application. The Policy Engine does not know anything about the application. It only compares queries to rules according to builtin evaluation rules for restricted S-expressions, as described in this document. What we are discussing in this section are the consequences of choosing certain meanings of a particular S-expression, given how the Policy Engine tests for the <=-relation. These properties of the Policy Engine must be fully understood by those deciding the structures of rules and queries.
When you have two hierarchies that are linked to each other it might be best to decouple them and make two lists of them. (role (org O OU_1 .. OU_n)(type R_1 .. R_m)) which gives you freedome to express the relationship "any role whithin a role hierarchy anywhere within a organization hierarchy".
(role (org UmU) (type admin finance)) <= (role (org UmU) (type admin)) (role (org UmU umdac) (type admin)) <= (role (org UmU) (type admin))
There is of course nothing that prevents you from using one nameform in one set of rules and another form in another as long as the queries you pose to the policy engine use the appropriate one. What you should make certain though is that the form you choose gives you the possibility to express exactly what you are aiming for.
The problem then becomes: who belongs to the organization ?
One often used solution when it is the sender that is checked is to make certain
that the ip address of the host used by the client belongs to the set of ip
addresses that the organization owns/uses.
One drawback of such a check is that anyone that is able to send a mail from
a machine within the organization is regarded as a member of the organization.
By some this might be regarded as a feature by other as a bug. We might also
have the situation that the organization may want to restrict the rights of
certain persons within the organization so that they can only send to certain
recipients or receive from specified senders. And of course anyone knows about
the much wanted possibility to restrict for instance spam from reaching users
mailboxes. But that problem is however not dealt with in this example.
So summing up we have three tests that should be performed before a mail is allowed to be forwarded by the organization's mailserver
(spocp (resource mailrelay)(action mail)(subject knownUnrestrictedSender))
(spocp (resource mailrelay)(action mail)(subject knownUnrestrictedReceiver))
(spocp (resource mailrelay)(action mail allowedReceiver)(subject knownRestrictedSender))
(spocp (resource mailrelay)(action mail allowedSender)(subject knownRestrictedReceiver))
Using the ldapset external reference, the test of "knownUnrestrictedSender" can be expressed as:
urn:spocp:ldapset:ldap.org.com;;dc=org,dc=com;{user$mail & ${sender}}/restricted & noSend
A corresponding query could then include:
(spocp ... (subject (sender torbjorn.wiberg@adm.umu.se) ... )))
Note ldap.org.com is assumed to be the name of the organization's ldapserver, and "dc=org,dc=com" is the root of the organization's subtree in that ldapserver.
The meaning of this rule is that if there is an entry in the enterprise directory with the sender's mail address as one of the values for the "mail" attribute and that entry also has the value "noSend" (remenber that this means "no send restriction") on the "restricted" attribute, that entry is regarded as representing a "known unrestricted sender". Given how easy it is to fake sender addresses in mails, one might want to add an extra test for sender host ipaddress. Thereby the subject part of the rule is expanded to be:
(subject
urn:spocp:ldapset:ldap.org.com;;dc=org,dc=com;{user$mail & ${sender}}/restricted & noSend
(ipnum (* range ipv4 <organizations ip number series>))
)
That is, not only must the sender be regarded as a knownUnstrictedSender, the mail must also originate from a machine within the organization.
To satisfy this rule, the corresponding query should include:
... (subject (sender torbjorn.wiberg@adm.umu.se)
(ipnum 193.195.52.1))
To cover the situation when users belonging to the organization use SMTP AUTH when not on premises, one could add an extra possibility (necessitates a slight rewrite of the above rule):
(subject
(* set
(smptauth)
(internal
urn:spocp:ldapset:ldap.org.com;;dc=org,dc=com;{user$mail & ${sender}}/restricted & noSend
(ipnum (* range ipv4 <organizations ip number series>))
)
)
)
What the mailrelay would send to the policy engine would be queries similar to these ones:
(spocp (resource mailrelay) (action mail) (subject (internal (sender roland@catalogix.se)(ipnum 193.195.52.1))) )
(spocp (resource mailrelay)(action mail)(subject (smtpauth roland)))
If the sender is not an unrestricted sender, another rule will be checked. Building that rule, again we assume that the restrictions are stored in the enterprise directory. Two checks need to be done in order to decide whether the sender is allowed to send to the specified recipient or not, if she is a restricted sender.
urn:spocp:ldapset:ldap.org.com;;dc=org,dc=com;{user$mail & ${sender}}/restricted & send
urn:spocp:ldapset:ldap.org.com;;dc=org,dc=com;{user$mail & ${sender}}/relayTo & ${receiver}
Combining the two tests we would get the following SPOCP rule:
(spocp
(resource mailrelay)
(action mail
urn:spocp:ldapset:ldap.org.com;;dc=org,dc=com;{user$mail & ${sender}}/relayTo & ${receiver}
))
(subject
(* set
(smptauth )
(internal
urn:spocp:ldapset:ldap.org.com;;dc=org,dc=com;{user$mail & ${sender}}/restricted & send
(ipnum (* range ipv4 <organizations ip number series>))
)
)
)
)
And the corresponding query would be something like this:
(spocp (resource mailrelay) (action mail (receiver leif@it.su.se)) (subject (internal (sender roland@catalogix.se)(ipnum 193.195.52.1))) )
Two tests are also needed to handle the receiver cases (knownUnrestrictedReceiver and knownRestrictedReceiver) but those are left as an excercise to the reader.
Perhaps there also ought to be a test that checked whether the uid that was received from the authentication is connected to the sender mail address, but that would be rather easy to add.
Note that there are limitations to this solution which probably makes it impractical to use in real life, so regard it only as an example. There is for instance a limitation in the ldapset external reference presently, that only allows you to define exact match tests, hence you can not specify a mail domain as one value for "relayTo" or "relayFrom" since the tests are always with the complete mail address. This will probably change in the future.