Compilation is done using stack
from
http://haskellstack.org. My current
stack release
version is 2.7.1
.
stack build --pedantic
stack run examples/AhnSandhuPaper2000.rcl
Via the file stack.yaml the compilation uses the latest
Glasgow haskell compiler version. Currently this is ghc-8.10.4
as
specified by the resolver version, see
https://www.stackage.org/. Compilation
should also succeed with other ghc versions (see
.travis.yml). Apart from the --pedantic
flag the
haskell sources are kept clean using the tools
hlint and
scan. Under windows you
should use the installer for
stack. For
properly displaying Unicode characters you may need to choose a proper
font of your command console. The haskell package
code-page is used to
switch to the Unicode code page 65001.
This work was inspired by Karsten Sohr who pointed me to the paper Role-based Authorization Constraints Specification from Gail-Joon Ahn and Ravi Sandhu published in 2000 as well as by my current project work regarding access control as part of (seaport) security and my former haskell programming work on Hets.
The Role-based Constraints Language RCL 2000 is supposed to provide a foundation for systematic studies and evaluations of role-based authorization constraints. After twenty years this implementation aims to provide the envisaged front-end that can be realistically used by security policy designers.
The abstract syntax tree of RCL 2000 as given in the paper is
reflected by the haskell data types in Ast. We
denote the function terms over sets simply as sets with type Set
and
avoid the words term, token and expression. Statements have the type
Stmt
and are actually Boolean formulas. For these recursive
data types handy folding functions are provided to shorten repetitive
recursive function definitions.
A parser for a list of statements is implemented in
Parse. The code uses the (parsec1
subset of the
old) haskell parsec
parser combinator library without separate
scanner. The parser accepts ASCII, LaTeX and Unicode variants of the
syntax. The (currently) accepted function names are user
, roles
,
sessions
, permissions
, operations
, and objects
. Note that
user
is singular and objects
is plural. Only the user
function
applied to a single session yields a single user. In all other cases
sets are returned, although objects
returns a singleton set if
applied to a single permission. Note that operations
is a binary
function and the only one (apart from set operations like union and
intersection). The application of unary functions can be written
with (as in the original paper) or without parentheses:
"AO(OE(CR))
" versus "AO OE CR
". (This is only similar to Haskell
as RCL is a first-order language and juxtaposition is made
right-associative.)
As in the paper the functions user
and roles
are overloaded
and functions roles
and permissions
have "*
" variants that
consider a role hierarchy. (The supported LaTeX suffix is "^{*}
".)
Also the general notational device for set-valued functions is
respected. This allows whole sets instead of single values as
arguments. (Only the user
function for sessions is not set-valued.)
Proper type checking is implemented in Type using a
simple state monad (from haskell package mtl
). The paper
mentions CR
, CP
, and CU
as sets of conflicting sets of roles,
permissions and users, respectively. However, these sets are not built
in but user defined and must therefore be supplied separately. The
types of such names are currently read from the file
examples/types.txt if the -t
option is
supplied via the command line. An alternative file path could be
appended to the -t
option. The command line interface for the binary
is defined in Cli.
The module Print allows to output parsed statements
to ASCII, LaTeX or Unicode text with or without redundant parentheses.
Pretty printed output can be successfully re-parsed, i.e. round tripped.
Printing uses the (good'n old) pretty
haskell package (see
dependencies
in package.yaml).
In Reduce the reduction to RFOPL and inverse (re-) construction from RFOPL is implemented. The construction is only needed for test purposes. Reduction only works for type correct statements and is a prerequisite for the translation ToOcl to OCL invariants for the USE-Tool (not described in the paper).
stack run -- -t -o. Stmts.rcl
use Stmts.use
The above call, where "stack run --
" could be the created binary
rcl200-exe
, would produce the file Stmts.use
from the statements
in the input file Stmts.rcl
(look into the examples
directory for .rcl
files). The input file Stmts.rcl
may not exist,
but the output file Stmts.use
will be created and would overwrite an
existing file. Without input statements no invariants would be
generated, though. Note the -t
option for proper type checking and
also be aware of the file use/RBAC.use that is always
the beginning of any created .use
file. The default output directory
for .use
files created by the -o
option is use. The option
-o.
puts these files into the current directory ".".
Within the USE-Tool concrete object diagrams could now be created and
the generated OCL invariants for a single RBAC
object could be
validated or refuted.
Yet, the implementation does not stop here. It allows to read in a full configuration as described below under the Usage heading.
The parser is more liberal than described in the paper:
-
some parentheses may be omitted.
-
set names can start with any Unicode letter. Further characters may be any alphanum or the underscore. Other characters may be easily allowed if they are not used elsewhere.
-
the minus sign
-
can be used for set difference: "AO(S)
" is the same as "S-{OE(S)}
" or "S - OE S
" as the curly braces will be derived by type checking. (The first part of the reduction is in fact the replacement ofAO
using-
, curly braces andOE
.) -
the function
user
may also be writtenusers
(plural) andobjects
asobject
(singular). -
explicit
juniors
andseniors
functions with and without*
as well as other partly overloaded functions have been added as described below. -
user defined sets may be overloaded and type annotated for disambiguation. "
U
" has type "Us
" and may be written as "U:Us
". Names are not restricted, even keywords or function names may be used as names of sets. Sometimes annotations, parentheses, commas, or semicolons are needed for proper parsing. -
(non-empty) sets can be constructed on the fly using curly braces. (In fact singleton sets are constructed during type checking and reduction.) A separating comma between elements of sets is only optional because the end of a set and the start of a second set can be recognized if no function names are used as base sets.
-
a
UP ⊆ U x P
history relation was added with functionsexecutions : U -> 2^P
andaccessors : P -> 2^U
:executions(u : U) = { p | (u, p) in UP } accessors(p : P) = { u | (u, p) in UP }
-
A statement may be preceded by
let
definitions:let v_1 = s_1 ; ... ; v_n = s_n in stmt
The variables v_i
shadow user defined sets (or equally named
variables v_j
with j < i
). A variable v_j
may be used in any set
s_i
with j < i
or in the actual statement stmt
. The semantics of
let
is a simple substitution of all variables from right to left in
stmt
. A variable v_i
may be type-annotated which is identical to a
type annotation of the set s_i
. (The semicolons in let
definitions
are also optional.)
The type checker is as liberal as described in the paper. Many elements are turned to singleton sets to ensure proper typing. The following reflexive transitive closure functions have been added:
juniors* : R -> 2^R, juniors*(r) = { j | j <= r }
seniors* : R -> 2^R, seniors*(r) = { s | r <= s }
So juniors*
can be used to compute all junior roles and seniors*
for
the senior roles including the argument role. With these two functions
the *
variants of other functions are strictly no longer necessary,
though still supported:
roles(u : U) = { r | (u, r) in UA }
roles(s : S) = defined activated roles of session s
roles(p : P) = { r | (p, r) in PA }
roles*(u : U) = juniors*(roles(u))
roles*(s : S) = juniors*(roles(s))
roles*(p : P) = seniors*(roles(p))
permissions(r : R) = { p | (p, r) in PA)
permissions*(r : R) = permission(juniors*(r))
The permissions
function is additionally overloaded and accepts user
and session arguments:
permissions(u : U) = permissions(roles(u))
permissions(s : S) = permissions(roles(s))
permissions*(u : U) = permissions*(roles(u))
permissions*(s : S) = permissions*(roles(s))
It should be noted that also the user
and operations
functions
must consider the role hierarchy as this is not mentioned (or wrong)
in the paper.
user(r : R) = { u | (u, r) in UA }
user*(r : R) = user(seniors*(r)) = { u | exist s >= r . (u, s) in UA }
operations(r : R, obj : OBJ) = { op | (op, obj, r) in PA }
operations*(r : R, obj : OBJ) = operations(juniors*(r), obj)
= { op | exist j <= r . (op, obj, j) in PA }
The user*
function also considers users that may have a role's
permissions due to their assignment to senior roles. The first
argument of operations
may also be a permission or a user:
operations(p : P, obj : OBJ) = { op | p = (op, obj) }
operations(r : R, obj : OBJ) = operations(permissions(r), obj)
operations*(r : R, obj : OBJ) = operations(permissions*(r), obj)
operations(u : U, obj : OBJ) = operations(roles(u), obj)
operations*(u : U, obj : OBJ) = operations*(roles(u), obj)
A permission is a pair consisting of an operation and an object. The
objects
function is a mere selector or projection that extracts the
unique object from a permission and just returns it as a singleton
set. The operations
function is different in that it takes an object
as second argument. If the first argument is a permission the result
is an empty set or a singleton set depending on the permission's
object. If the first argument is a role all operations from the role's
permissions that operate on the given object are returned. Note, how
simple the original operations
function for roles can be expressed
using the added operations
function for permissions. The
operations*
functions additionally consider the permissions due to
junior roles of the input role. No *
is allowed for the simple
objects
function and the operations
function applied to
permissions!
A session has a unique name, belongs to a unique user, and comprises a
set of so called activated roles. The overloaded function user
(where *
would be illegal) returns the unique user of the session
given as input. (In case of need this single user is converted into a
singleton user set.) The inverse function sessions
(where *
is
also illegal) returns a proper set of sessions namely all sessions
belonging to the user given as input. This set may be empty, a
singleton set or a larger set, as a single user may open several
sessions.
user(s) = defined unique user of session s
sessions(u) = { s | user(s) = u }
The roles
function applied to a session returns the explicitly
activated roles of this session. These activated roles must be a
subset of the roles authorized by the session's user. Simply
requiring roles(s)
to be a subset of roles(user(s))
for a session
s
is not good enough in the presence of role hierarchies. Any role
from roles*(user(s))
may be activated in session s
and any role
from roles*(s)
will be activated either explicitly or implicitly.
An RCL statement to express the required subset relation between
activated and authorized roles would be the following, but this
relation is enforced internally:
OE(roles(OE(S))) ∈ roles*(user(OE(S)))
In addition to the juniors*
and seniors*
functions also juniors
and seniors
functions without *
are provided. These functions
denote the immediate junior or senior roles of an input role. It is
the irreflexive transitive reduction of the role hierarchy. The
immediate subrole relation is denoted using <<
and is a subrelation
of <=
.
juniors : R -> 2^R, juniors(r) = { j | j << r }
seniors : R -> 2^R, seniors(r) = { s | r << s }
With these functions also limited role hierarchies could be
specified using RCL. The statement "|juniors(OE(R))| <= 1
" would
restrict any role in the role hierarchy to have at most a single
immediate junior role. Such a hierarchy would be a forest of trees
with roots at the bottom. A reversed limited hierarchy with roots at
the top would result when using seniors
for single immediate
seniors. In practice this may be rarely useful, though.
A statement "|juniors(OE(R))| = 0
" or "|juniors*(OE(R))| = 1
" or
equivalently for seniors
would even prohibit a role hierarchy! This
demonstrates the expressive power of RCL with these additional
juniors
and seniors
functions.
The conflict sets CR
, CP
, or CU
described in the paper are
set of sets of R
, P
, or U
(with types Rss
, Pss
, or Uss
),
respectively. These sets can be used to describe separation of duty
(SoD) constraints in RCL.
|roles*(OE(U)) ∩ OE(CR)| ≤ 1
The authorized roles of any user OE(U)
may at most contain a single
role from any conflict set OE(CR)
. In the case of two conflicting
roles r1
and r2
and a singleton set of a conflict set
(CR={{r1,r2}}
), no user u=OE(U)
from U
could be assigned (or
authorized) to both roles without violating the
constraint. OE(CR)={r1,r2}
would be a subset of roles*(u)
thus the
intersection would contain two elements rather than at most one.
For a role hierarchy the function roles*
must be used in the above
statement. However, it does not make sense to put a junior and senior
role into one conflict set, because this would imply that only the
junior role could be authorized. If the senior role is authorized then
the junior role is authorized implicitly yielding the unintended
conflict. In fact no roles with common senior roles should be put into
conflict sets! Singleton (or even empty) conflict sets as elements of
CR
also do not make sense as for these sets the intersection will
always be at most one element.
The constraint to exclude conflicting roles via common senior roles could be given as:
let cr = OE(CR); r1 = OE(cr); r2 = OE(AO(cr))
in seniors*(r1) ∩ seniors*(r2) = ∅
The construction OE(AO(cr))
takes a second role r2
from cr
that is
different from r1
.
The paper distinguishes static (SSoD) and dynamic (DSoD) separation of duty. SSoD addresses assigned (or with a role hierarchy authorized) roles whereas DSoD addresses the activated roles of sessions as in the following statement.
|roles*(sessions(OE(U))) ∩ OE(CR)| ≤ 1
It should be noted that the set of conflict sets CR
can not be used
if static and dynamic SoD constraints should be enforced
simultaneously, because a static constraint already rules out the
mere activation of conflicting roles in sessions. For both, SSoD and
DSoD, two different sets of sets need to and can be defined,
i.e. SCR
and DCR
. CR
(as well as CP
or CU
) are not builtin
but user defined. SCR
and DCR
can be defined in the
types file in type checking (-t
) mode or in the
sets file.
Usually several permissions are assigned to a single role. In order to
avoid conflicts maybe more intuitively, a set of conflicting
permission sets like CP
may be used.
|permissions(roles*(OE(U))) ∩ OE(CP)| ≤ 1
Here the use of permissions*
is not required as roles*
already
transitively closes the role sets. The same statement can be expressed
equivalently.
|permissions*(roles(OE(U))) ∩ OE(CP)| ≤ 1
Using *
for both functions would also not change the semantics. To
switch the static constraint into a dynamic one, again roles from
sessions
need to be taken.
|permissions(roles*(sessions(OE(U)))) ∩ OE(CP)| ≤ 1
The difference between permissions and roles is not that striking as
permission conflicts imply role conflicts. Again it is no good idea to
assign conflicting permissions to roles comparable via <=
. Assigning
conflicting permissions to a single role or comparable roles can be
avoided by the following constraint.
|permissions*(OE(R)) ∩ OE(CP)| ≤ 1
The set CU
can be used to restrict badly collaborating users. The
interactions with conflicting roles and the presence of role
hierarchies may be difficult to grasp, though, and is therefore left
to the literature or as an exercise.
Comparison with the RBAC ANSI INCITS 359
RBAC ANSI has a couple of review functions for queries that correspond to the above RCL functions as follows.
RC-11 AssignedUsers(r, result) <=> result = users(r)
RC-09 AssignedRoles(u, result) <=> result = roles(u)
RC-34 RolePermissions(r, result) <=> result = permissions*(r)
RC-43 UserPermissions(u, result) <=> result = permissions*(u)
RC-36 SessionRoles(s, result) <=> result = roles*(s)
RC-35 SessionPermissions(s, result) <=> result = permissions*(s)
RC-33 RoleOperationsOnObject(r, obj, result) <=> result = operations*(r, obj)
RC-42 UserOperationsOnObject(u, obj, result) <=> result = operations*(u, obj)
RC-12 AuthorizedUsers(r, result) <=> result = users*(r)
RC-13 AuthorizedRoles(u, result) <=> result = roles*(u)
SessionRoles
and SessionPermissions
must consider a possible role
hierarchy. (This seems to be wrong in ANSI INCITS 359.) The names
for ...Permissions
and ...OperationsOnObject
are used with and
without a role hierarchy whereas ...Users
and ...Roles
are
prefixed using Assigned...
without and using Authorized...
with a
role hierarchy.
The ANSI review functions complement administrative commands and supporting system functions for creating and managing RBAC elements. Currently, RBAC elements for RCL are initially read from files as described below.
RBAC ANSI also describes static and dynamic separation of duty
relations. A static SoD (SSD) addresses authorized roles of a user
(AssignedRoles/AuthorizedRoles
) wheres a dynamic SoD (DSD) addresses
the activated roles of a session (SessionRoles
). For any SoD a
named set of roles can be associated with a number between 2 and the
cardinality of the set. If this number of roles from the given set are
taken simultaneously the SoD constraint is violated. Given a number
(n>=2
) and a set of roles (cr
) with at least n
elements, then
at most n-1
roles of this set may be assigned/authorized (SSD) or
activated (DSD). Using RCL we can express this by:
|roles*(OE(U)) ∩ cr| < n // SSD
|roles*(OE(S)) ∩ cr| < n // DSD
Note that we use roles*
also for sessions. (Therefore SessionRoles
must consider a role hierarchy, too.) Furthermore, the second DSD does
not prevent a user to take n
(or more) roles via several
sessions. In order to prevent the latter the RCL constraint would need
to be:
|roles*(sessions(OE(U))) ∩ cr| < n // DSD
For different numbers n
different set of set of roles could be
defined, like the above conflicting roles set CR
for
n=2
. Summarizing, RCL is also able or even better suited to express
typical static or dynamic SoD constraints.
find . -name rcl2000-exe
# find the binary and put it into your PATH
# or use "stack run --" or "stack exec rcl2000-exe --" instead of "rcl2000-exe"
rcl2000-exe -o. Stmts.rcl
use Stmts.use Stmts.soil
The above call relies on the files
rh, ua, pa, pu, s, and sets from the examples
directory. The default file extension is .txt
, but could be changed
using the -x
option.
rh contains roles, usually one role on a separate line. Several roles on one line define a senior role and all (direct) junior roles of this senior role.
ua introduces all users with there (statically) assigned roles. This defines the user-to-role assignment or a single user if there is only one name in a line.
pa defines the permission-to-role assignment. Each permission is given by two leading words of a line. The first word is the operation and second the object of the permission. The internal name of a permission will be simply the operation joined with the object by an (additional) underscore.
pu defines the history of users that executed certain permissions. Each permission is given by two leading words of a line. The first word is the operation and second the object of the permission. All other words of the same line are users.
s contains users sessions. A line starts with a session id and a user name and all remaining words are the associated activated roles. Note that the activated roles must be a subset of the assigned/authorized roles of the user.
sets contains user defined sets of known
entities like roles, users, etc. The leading word in a line must a new
name, all following words must be known and they must be of the same
type to form a new homogeneous set with the given elements. This way
sets of sets can be defined to describe conflicts like CR
. The
actual types of user defined names is no longer taken from the
types file but derived from these set
definitions.
When the above files are read some sanity checks are performed that are reported. The hopefully informative messages should be inspected. (Run time errors that point to source locations, should be reported with instructions to reproduce them.)
Provided the generated .use
and .soil
files are successfully
loaded by use, the tool buttons to show class- and object
diagrams as well as class invariants should work. The generated
structure (menu State
) should also be fine.
The class invariants generated from the input file should validate in
the same way as the internal evaluation that is triggered using the
-e
option.
The -i
option sends rcl2000 into interactive mode that can be
terminated by entering q
or pressing Ctrl-C
(interrupt) or
Ctrl-D
(end of input). You may also try to enter h
for help or
some set expressions. This feature is still provisional.
The -h
option displays a usage message.
The commands, sets, or statements entered at the prompt are kept in a
history file named .haskeline_history
. If a file .haskeline
containing historyDuplicates: IgnoreAll
exists, then duplicate
entries are avoided. This functionality is provided by the haskell
package haskeline.