From 2bb10e03b13c7ff2bc821a2e44e3ac33326c62f5 Mon Sep 17 00:00:00 2001 From: John Dennis Date: Sun, 2 Nov 2014 08:39:32 -0500 Subject: Initial import of files --- doc/mapping.rst | 1609 ++++++++++++++++++++ .../main/java/com/redhat/IdPMapping/IdpJson.java | 254 +++ .../redhat/IdPMapping/InvalidRuleException.java | 34 + .../redhat/IdPMapping/InvalidTypeException.java | 35 + .../redhat/IdPMapping/InvalidValueException.java | 34 + .../java/com/redhat/IdPMapping/RuleProcessor.java | 1382 +++++++++++++++++ .../redhat/IdPMapping/StatementErrorException.java | 34 + .../src/main/java/com/redhat/IdPMapping/Token.java | 406 +++++ .../redhat/IdPMapping/UndefinedValueException.java | 34 + java/src/main/java/com/redhat/app/MappingApp.java | 57 + python/idp_mapping.py | 1055 +++++++++++++ python/mapping_app.py | 55 + 12 files changed, 4989 insertions(+) create mode 100644 doc/mapping.rst create mode 100644 java/src/main/java/com/redhat/IdPMapping/IdpJson.java create mode 100644 java/src/main/java/com/redhat/IdPMapping/InvalidRuleException.java create mode 100644 java/src/main/java/com/redhat/IdPMapping/InvalidTypeException.java create mode 100644 java/src/main/java/com/redhat/IdPMapping/InvalidValueException.java create mode 100644 java/src/main/java/com/redhat/IdPMapping/RuleProcessor.java create mode 100644 java/src/main/java/com/redhat/IdPMapping/StatementErrorException.java create mode 100644 java/src/main/java/com/redhat/IdPMapping/Token.java create mode 100644 java/src/main/java/com/redhat/IdPMapping/UndefinedValueException.java create mode 100644 java/src/main/java/com/redhat/app/MappingApp.java create mode 100755 python/idp_mapping.py create mode 100755 python/mapping_app.py diff --git a/doc/mapping.rst b/doc/mapping.rst new file mode 100644 index 0000000..3363550 --- /dev/null +++ b/doc/mapping.rst @@ -0,0 +1,1609 @@ +Operation Model +=============== + +The assertions from an IdP are stored in an associative array. A +sequence of rules are applied, the first rule which returns success is +considered a match. During the execution of each rule values from the +assertion can be tested and transformed with the results selectively +stored in variables local to the rule. If the rule succeeds an +associative array of mapped values is returned. The mapped values are +taken from the local variables set during the rule execution. The +definition of the rules and mapped results are expressed in JSON +notation. + +A rule is somewhat akin to a function in a programming language. It +starts execution with a set of predefined local variables. It executes +statements which are grouped together in blocks. Execution continues +until an `exit`_ statement returning a success/fail result is +executed or until the last statement is reached which implies +success. The remaining statements in a block may be skipped via a +`continue`_ statement which tests a condition, this is equivalent to +an "if" control flow of logic in a programming language. + +Rule execution continues until a rule returns success. Each rule has a +`mapping`_ associative array bound to it which is a template for the +transformed result. Upon success the `mapping`_ template for the +rule is loaded and the local variables from the successful rule are +used to populate the values in the `mapping`_ template yielding the +final mapped result. + +If no rules returns success authentication fails. + + +Pseudo Code Illustrating Operational Model +------------------------------------------ + +:: + + mapped = null + foreach rule in rules { + result = null + initialize rule.variables with pre-defined values + + foreach block in rule.statement_blocks { + for statement in block.statements { + if statement.verb is exit { + result = exit.status + break + } + elif statement.verb is continue { + break + } + } + if result { + break + } + if result == null { + result = success + } + if result == success { + mapped = rule.mapping(rule.variables) + } + return mapped + + + +Structure Of Rule Definitions +============================= + +Rules are loaded by the rule processor via a JSON document called a +rule definition. A definition has an *optional* set of mapping +templates and a list of rules. Each rule has specifies a mapping +template and has a list of statement blocks. Each statement block has +a list of statements. + +In pseudo-JSON (JSON does not have comments, the ... ellipsis is a +place holder): + +:: + + { + "mappings": { + "template1": "{...}", + "template2": "{...}" + }, + "rules": [ + { # Rule 0. A rule has a mapping or a mapping name + # and a list of statement blocks + + "mapping": {...}, + # -OR- + "mapping_name": "template1", + + "statement_blocks": [ + [ # Block 0 + [statement 0] + [statement 1] + ], + [ # Block 1 + [statement 0] + [statement 1] + ], + + ] + }, + { # Rule 1 ... + } + ] + + } + +Mapping +------- + +A mapping template is used to produce the final associative array of +name/value pairs. The template is a JSON Object. The value in a +name/value pair can be a constant or a variable. If the template value +is a variable the value of the variable is retrieved from the set of +local variables bound to the rule thereby replacing it in the final +result. + +For example given this mapping template and rule variables in JSON: + +template: + +:: + + { + "organization": "BigCorp.com", + "user: "$subject", + "roles": "$roles" + } + +local variables: + +:: + + { + "subject": "Sally", + "roles": ["user", "admin"] + } + +The final mapped results would be: + +:: + + { + "organization": "BigCorp.com", + "user: "Sally", + "roles": ["user", "admin"] + } + + +Each rule must bind a mapping template to the rule. The mapping +template may either be defined directly in the rule via the +``mapping`` key or referenced by name via the ``mapping_name`` key. + +If the ``mapping_name`` is specified the mapping is looked up in a +table of mapping templates bound to the Rule Processor. Using the name +of a mapping template is useful when many rules generate the exact +same template values. + +If both ``mapping`` and ``mapping_name`` are defined the locally bound +``mapping`` takes precedence. + +Syntax +------ + +The logic for a rule consists of a sequence of statements grouped in +blocks. A statement is similar to a function call in a programming +language. + +A statement is a list of values the first of which is a verb which +defines the operation the statement will perform. Think of the +`verbs`_ as function names or operators. Following the verb are +parameters which may be constants or variables. If the statement +assigns a value to a variable left hand side of the assignment (lhs) +is always the first parameter following the verb in the list of +statement values. + +For example this statement in JSON: + +:: + + ["split", "$groups", "$assertion[Groups]", ":"] + +will assign an array to the variable ``$groups``. It looks up the +string named ``Groups`` in the assertion which is a colon (:) +separated list of group names splitting that string on the colon +character. + +Statements **must** be grouped together in blocks. Therefore a rule is +a sequence of blocks and block is a sequence of statements. The +purpose of blocks is allow for crude flow of control logic. For +example this JSON rule has 4 blocks. + +:: + + [ + [ + ["set", $user, ""], + ["set", $roles, []] + ], + [ + ["in", "UserName", "$assertion"], + ["continue", "if_not_success"], + ["set", "$user", "$assertion[UserName"], + ], + [ + ["in", "subject", "$assertion"], + ["continue", "if_not_success"], + ["set", "$user", "$assertion[subject]"], + ], + [ + ["length", "$temp", "$user"], + ["compare", "$temp", ">", 0], + ["exit", "rule_fails", "if_not_success"] + ["append" "$roles", "unprivileged"] + ] + ] + +The rule will succeed if either ``UserName`` or ``subject`` is defined +in the assertion and if so the local variable ``$user`` will be set to +the value found in the assertion and the "unprivileged" role will be +appended to the roles array. + +The first block performs initialization. The second block tests to see +if the assertion has the key ``UserName`` if not execution continues +at the next block otherwise the value of UserName in the assertion is +copied into the variable ``$user``. The third block performs a similar +operation looking for a ``subject`` in the assertion. The fourth block +checks to see if the ``$user`` variable is empty, if it is empty the +rule fails because it didn't find either a ``UserName`` nor a +``subject`` in the assertion. If ``$user`` is not empty the +"unprivileged" role is appended and the rule succeeds. + +Data Types +---------- + +There are 7 supported types which equate to the types available in +JSON. At the time of this writing there are 2 implementations of this +Mapping specification, one in Python and one in Java. This table +illustrates how each data type is represented. The first two columns +are definitions from an abstract specification. The JSON column +enumerates the data type JSON supports. The Mapping column lists the +7 enumeration names used by the Mapping implemenation in each +language. The following columns list the concrete data type used in +that language. + ++-----------+------------+--------------------+---------------------+ +| JSON | Mapping | Python | Java | ++===========+============+====================+=====================+ +| object | MAP | dict | Map | ++-----------+------------+--------------------+---------------------+ +| array | ARRAY | list | List | ++-----------+------------+--------------------+---------------------+ +| string | STRING | unicode (Python 2) | String | +| | +--------------------+ | +| | | str (Python 3) | | ++-----------+------------+--------------------+---------------------+ +| | INTEGER | int | Long | +| number +------------+--------------------+---------------------+ +| | REAL | float | Double | ++-----------+------------+--------------------+---------------------+ +| true | | | | ++-----------+ BOOLEAN | bool | Boolean | +| false | | | | ++-----------+------------+--------------------+---------------------+ +| null | NULL | None | null | ++-----------+------------+--------------------+---------------------+ + + +Rule Debugging and Documentation +-------------------------------- + +If the rule processor reports an error or if you're debugging your +rules by enabling DEBUG log tracing then you must be able to correlate +the reported statement to where it appears in your rule JSON source. A +message will always identify a statement by the rule number, block +number within that rule and the statement number within that +block. However once your rules become moderately complex it will +become increasingly difficult to identify a statement by counting +rules, blocks and statements. + +A better approach is to tag rules and blocks with a name or other +identifying string. You can set the `Reserved Variables`_ +``rule_name`` and ``block_name`` to a string of your choice. These +strings will be reported in all messages along with the rule, block +and statement numbers. + +JSON does not permit comments, as such you cannot include explanatory +comments next to your rules, blocks and statements in the JSON +source. The ``rule_name`` and ``block_name`` can serve a similar +purpose. By putting assignments to these variables as the first +statement in a block you'll both document your rules and be able to +identify specific statements in log messages. + +During rule execution the ``rule_name`` and ``block_name`` are +initialized to the empty string at the beginning of each rule and +block respectively. + +The above example is augmented to include this information. The rule +name is set in the first statement in the first block. + +:: + + [ + [ + ["set", "$rule_name", "Must have UserName or subject"], + ["set", "block_name", "Initialization"], + ["set", $user, ""], + ["set", $roles, []] + ], + [ + ["set", "block_name", "Test for UserName, set $user"], + ["in", "UserName", "$assertion"], + ["continue", "if_not_success"], + ["set", "$user", "$assertion[UserName"], + ], + [ + ["set", "block_name", "Test for subject, set $user"], + ["in", "subject", "$assertion"], + ["continue", "if_not_success"], + ["set", "$user", "$assertion[subject]"], + ], + [ + ["set", "block_name", "If not $user fail, else append unprivileged to roles"], + ["length", "$temp", "$user"], + ["compare", "$temp", ">", 0], + ["exit", "rule_fails", "if_not_success"] + ["append" "$roles", "unprivileged"] + ] + ] + + + + +Variables +--------- + + +Variables always begin with a dollar sign ($) and are followed by an +identifier which is any alpha character followed by zero or more +alphanumeric or underscore characters. The variable may optionally be +delimited with braces ({}) to separate the variable from surrounding +text. Three types of variables are supported: + +* scalar +* array (indexed by zero based integer) +* associative array (indexed by string) + +Both arrays and associative arrays use square brackets ([]) to specify +a member of the array. Examples of variable usage: + +:: + + $name + ${name} + $groups[0] + ${groups[0]} + $properties[key] + ${properties[key]} + +An array or an associative array may be referenced by it's base name +(omitting the indexing brackets). For example the associative array +array named "properties" is referenced using it's base name +``$properties`` but if you want to access a member of the "properties" +associative array named "duration" you would do this ``$properties[duration]`` + +This is not a general purpose language with full expression +syntax. Only one level of variable lookup is supported. Therefore +compound references like this + +:: + + $properties[$groups[2]] + +will not work. + + +Escaping +^^^^^^^^ + +If you need to include a dollar sign in a string (where it is +immediately followed by either an identifier or a brace and identifier) +and do not want to have it be interpreted as representing a variable +you must escape the dollar sign with a backslash, for example +"$amount" is interpreted as the variable ``amount`` but "\\$amount" +is interpreted as the string "$amount" . + + +Reserved Variables +------------------ + +A rule has the following reserved variables: + +assertion + The current assertion values from the federated IdP. It is a + dictionary of key/value pairs. + +regexp_array + The regular expression groups from the last successful regexp match + indexed by number. Group 0 is the entire match. Groups 1..n are + the corresponding parenthesized group counting from the left. For + example regexp_array[1] is the first group. + +regexp_map + The regular expression groups from the last successful regexp match + indexed by group name. + +rule_number + The zero based index of the currently executing rule. + +rule_name + The name of the currently executing rule. If the rule name has not + been set it will be the empty string. + +block_number + The zero based index of the currently executing block within the + currently executing rule. + +block_name + The name of the currently executing block. If the block name has not + been set it will be the empty string. + + +statement_number + The zero based index of the currently executing statement within the + currently executing block. + + +Examples +======== + +Split a fully qualified username into user and realm components +--------------------------------------------------------------- + +It's common for some IdP's to return a fully qualified username +(e.g. principal or subject). The fully qualified username is the +concatenation of the user name, separator and realm name. A common +separator is the @ character. In this example lets say the fully +qualified username is ``bob@example.com`` and you want to return the +user and realm as independent values in your mapped result. The +username appears in the assertion as the value ``Principal``. + +Our strategy will be to use a regular expression identify the user and +realm components and then assign them to local variables which will +then populate the mapped result. + +The mapping in JSON is: + +:: + + { + "user": "$username", + "realm": "$domain" + } + +The assertion in JSON is: + +:: + + { + "Principal": "bob@example.com" + } + +Our rule is: + +:: + + [ + [ + ["in", "Principal", "assertion"], + ["exit", "rule_fails", "if_not_success"], + ["regexp", "$assertion[Principal]", (?P\\w+)@(?P.+)"], + ["set", "$username", "$regexp_map[username]"], + ["set", "$domain", "$regexp_map[domain]"], + ["exit, "rule_succeeds", "always"] + ] + ] + +Rule explanation: + +Block 0: + +0. Test if the assertion contains a Principal value. +1. Abort the rule if the assertion does not contain a Principal + value. +2. Apply a regular expression the the Principal value. Use named + groupings for the username and domain components for clarity. +3. Assign the regexp group username to the $username local variable. +4. Assign the regexp group domain to the $domain local variable. +5. Exit the rule, apply the mapping, return the mapped values. Note, an + explicit `exit`_ is not required if there are no further statements + in the rule, as is the case here. + +The mapped result in JSON is: + +:: + + { + "user": "bob", + "realm": "example.com" + } + +Build a set of roles based on group membership +---------------------------------------------- + +Often one wants to grant roles to a user based on their membership in +certain groups. In this example let's say the assertion contains a +``Groups`` value which is a colon separated list of group names. Our +strategy is to split the ``Groups`` assertion value into an array of +group names. Then we'll test if a specific group is in the groups +array, if it is we'll add a role. Finally if no roles have been mapped +we fail. Users in the group "student" will get the role "unprivileged" +and users in the group "helpdesk" will get the role "admin". + +The mapping in JSON is: + +:: + + { + "roles": "$roles", + } + +The assertion in JSON is: + +:: + + { + "Groups": "student:helpdesk" + } + +Our rule is: + +:: + + [ + [ + ["in", "Groups", "assertion"], + ["exit", "rule_fails", "if_not_success"], + ["set", "$roles", []], + ["split", "$groups", "$assertion[Groups]", ":"], + ], + [ + ["in", "student", "$groups"], + ["continue", "if_not_success"], + ["append", "$roles", "unprivileged"] + ], + [ + ["in", "helpdesk", "$groups"], + ["continue", "if_not_success"], + ["append", "$roles", "admin"] + ], + [ + ["unique", "$roles", "$roles"], + ["length", "$temp", "roles"], + ["compare", $temp", ">", 0], + ["exit", "rule_fails", "if_not_success"] + ] + + ] + +Rule explanation: + +Block 0 + +0. Test if the assertion contains a Groups value. +1. Abort the rule if the assertion does not contain a Groups + value. +2. Initialize the $roles variable to an empty array. +3. Split the colon separated list of group names into an array of + individual group names + +Block 1 + +0. Test if "student" is in the $groups array +1. Exit the block if it's not. +2. Append "unprivileged" to the $roles array + +Block 2 + +0. Test if "helpdesk" is in the $groups array +1. Exit the block if it's not. +2. Append "admin" to the $roles array + +Block 3 + +0. Strip any duplicate roles that might have been appended to the + $roles array to assure each role is unique. +1. Count how many members are in the $roles array, assign the + length to the $temp variable. +2. Test to see if the $roles array had any members. +3. Fail if no roles had been assigned. + +The mapped result in JSON is: + +:: + + { + "roles": ["unprivileged", "admin"] + } + +However, suppose whatever is receiving your mapped results is not +expecting an array of roles. Instead it expects a comma separated list +in a string. To accomplish this add the following statement as the +last one in the final block: + +:: + + ["join", "$roles", "$roles", ","] + +Then the mapped result will be: + +:: + + { + "roles": "unprivileged,admin"] + } + + + + +White list certain users and grant them specific roles +------------------------------------------------------ + +Suppose you have certain users you always want to unconditionally +accept and authorize with specific roles. For example if the user is +"head_of_IT" then assign her the "user" and "admin" roles. Otherwise +keep processing. The list of white listed users is hard-coded into the +rule. + +The mapping in JSON is: + +:: + + { + "user": $user, + "roles": "$roles", + } + +The assertion in JSON is: + +:: + + { + "UserName": "head_of_IT" + } + +Our rule in JSON is: + +:: + + [ + [ + ["in", "UserName", "assertion"], + ["exit", "rule_fails", "if_not_success"], + ["in", "$assertion[UserName]", ["head_of_IT", "head_of_Engineering"]], + ["continue", "if_not_success"], + ["set", "$user", "$assertion[UserName"] + ["set", "$roles", ["user", "admin"]], + ["exit", "rule_succeeds", "always"] + ], + [ + ... + ] + ] + +Rule explanation: + +Block 0 + +0. Test if the assertion contains a UserName value. +1. Abort the rule if the assertion does not contain a UserName + value. +2. Test if the user is in the hardcoded list of white listed users. +3. If the user isn't in the white listed array then exit the block and + continue execution at the next block. +4. Set the $user local variable to $assertion[UserName] +5. Set the $roles local variable to the hardcoded array containing + "user" and "admin" +6. We're done, unconditionally exit and return the mapped result. + +Block 1 + +0. Further processing + +The mapped result in JSON is: + +:: + + { + "user": "head_of_IT", + "roles": ["users", "admin"] + } + + +Black list certain users +------------------------ + +Suppose you have certain users you always want to unconditionally +deny access to by placing them in a black list. In this example the +user "BlackHat" will try to gain access. The black list includes the +users "BlackHat" and "Spook". + +The mapping in JSON is: + +:: + + { + "user": $user, + "roles": "$roles", + } + +The assertion in JSON is: + +:: + + { + "UserName": "BlackHat" + } + +Our rule in JSON is: + +:: + + [ + [ + ["in", "UserName", "assertion"], + ["exit", "rule_fails", "if_not_success"], + ["in", "$assertion[UserName]", ["BlackHat", "Spook"]], + ["exit", "rule_fails", "if_success"] + ], + [ + ... + ] + ] + +Rule explanation: + +Block 0 + +0. Test if the assertion contains a UserName value. +1. Abort the rule if the assertion does not contain a UserName + value. +2. Test if the user is in the hard-coded list of black listed users. +3. If the test succeeds then immediately abort and return failure. + +Block 1 + +0. Further processing + +The mapped result in JSON is: + +:: + + Null + +Format Strings and/or Concatenate Strings +----------------------------------------- + +You can replace variables in a format string using the `interpolate`_ +verb. String concatenation is trivially placing two variables adjacent +to one another in a format string. Suppose you want to form an email +address from the username and domain in an assertion. + +The mapping in JSON is: + +:: + + { + "email": $email, + } + +The assertion in JSON is: + +:: + + { + "UserName": "Bob", + "Domain": "example.com" + } + +Our rule in JSON is: + +:: + + [ + [ + ["interpolate", "$email", "$assertion[UserName]@$assertion[Domain]"], + ] + ] + +Rule explanation: + +Block 0 + +0. Replace the variable $assertion[UserName] with it's value and + replace the variable $assertion[Domain] with it's value. + +The mapped result in JSON is: + +:: + + { + "email": "Bob@example.com", + } + + +Note, sometimes it's necessary to utilize braces to separate variables +from surrounding text by using the brace notation. This can also make +the format string more readable. Using braces to delimit variables the +above would be: + +:: + + [ + [ + ["interpolate", "$email", "${assertion[UserName]}@${assertion[Domain]}"], + ] + ] + + + +Make associative array lookups case insensitive +----------------------------------------------- + +Many systems treat field names as case insensitive. By default +associative array indexing is case sensitive. The solution is to lower +case all the keys in an associative array and then only use lower case +indices. Suppose you want the assertion associative array to be case +insensitive. + +The mapping in JSON is: + +:: + + { + "user": $user, + } + +The assertion in JSON is: + +:: + + { + "UserName": "Bob" + } + +Our rule in JSON is: + +:: + + [ + [ + ["lower", "$assertion", "$assertion"], + ["in", "username", "assertion"], + ["exit", "rule_fails", "if_not_success"], + ["set", "$user", "$assertion[username"] + ] + ] + +Rule explanation: + +Block 0 + +0. Lower case all the keys in the assertion associative array. +1. Test if the assertion contains a username value. +2. Abort the rule if the assertion does not contain a username + value. +3. Assign the username value in the assertion to $user + +The mapped result in JSON is: + +:: + + { + "user": "Bob", + } + + +Verbs +===== + +The following verbs are supported: + +* `set`_ +* `length`_ +* `interpolate`_ +* `append`_ +* `unique`_ +* `regexp`_ +* `regexp_replace`_ +* `split`_ +* `join`_ +* `lower`_ +* `upper`_ +* `compare`_ +* `in`_ +* `not_in`_ +* `exit`_ +* `continue`_ + +Some verbs have a side effects. A verb may set a boolean success/fail +result which may then be tested with a subsequent verb. For example +the ``fail`` verb can be used to indicate the rule fails if a prior +result is either ``success`` or ``not_success``. The ``regexp`` verb +which performs a regular expression search on a string stores the +regular expression sub-matches as a side effect in the variables +``$regexp_array`` and ``$regexp_map``. + + +Verb Definitions +================ + +set +--- + +``set $variable value`` + +$variable + The variable being assigned (i.e. lhs) + +value + The value to assign to the variable (i.e. rhs). The value may be + another variable or a constant. + +**set** assigns a value to a variable, in other words it's an +assignment statement. + +Examples: +^^^^^^^^^ + +Initialize a variable to an empty array. + +:: + + ["set", "$groups", []] + +Initialize a variable to an empty associative array. + +:: + + ["set", "$groups", {}] + +Assign a string. + +:: + + ["set", "$version", "1.2.3"] + +Copy the ``UserName`` value from the assertion to a temporary variable. + +:: + + ["set", "$temp", "$assertion[UserName]"], + + +Get the 2nd item in an array (array indexing is zero based) + +:: + + ["set", "$group", "$groups[1]"] + + +Set the associative array entry "IdP" to "kdc.example.com". + +:: + + ["set", "$metadata[IdP]", "kdc.example.com""] + +-------------------------------------------------------------------------------- + +length +------ + +``length $variable value`` + +$variable + The variable which receives the length value + +value + The value whose length is to be determined. May be one of array, + associative array, or string. + +**length** computes the number of items in the value. How this is done +depends upon the type of value: + +array + The length is the number of items in the array. + +associative array + The length is the number of key/value pairs in the associative + array. + +string + The length is the number of *characters* (not octets) in the + string. + +Examples: +^^^^^^^^^ + +Count how many items are in the ``$groups`` array and assign that +value to the ``$groups_length`` variable. + +:: + + ["length", "$groups_length", "$groups"] + +Count how many key/value pairs are in the ``$assertion`` associative +array and assign that value to the ``$num_assertion_values`` variable. + +:: + + ["length", "$num_assertion_values", "$assertion"] + +Count how many characters are in the assertion's UserName and assign +the value to ``$username_length``. + +:: + + ["length", "$user_name_length", "$assertion[UserName]"] + + +-------------------------------------------------------------------------------- + +interpolate +----------- + +``interpolate $variable string`` + +$variable + This variable is assigned the result of the interpolation. + +string + A string containing references to variables which will be replaced + in the string. + +**interpolate** replaces each occurrence of a variable in a string with +it's value. The result is assigned to $variable. + +Examples: +^^^^^^^^^ + +Form an email address given the username and domain. If the username +is "jane" and the domain is "example.com" then $email will be +"jane@example.com" + +:: + + ["interpolate", "$email", "${username}@${domain}"] + + +-------------------------------------------------------------------------------- + + +append +------ + +``append $variable value`` + +$variable + This variable **must** be an array. It is modified in place by + appending ``value`` to the end of the array. + +value + The value to append to the end of the array. + +**append** adds a value to end of an array. + +Examples: +^^^^^^^^^ + +Append the role "qa_test" to the roles list. + +:: + + ["append", "$roles", "qa_test"] + + +-------------------------------------------------------------------------------- + + +unique +------ + +``unique $variable value`` + +$variable + This variable is assigned the unique values in the ``value`` + array. + +value + An array of values. **must** be an array. + +**unique** builds an array of unique values in ``value`` by stripping +out duplicates and assigns the array of unique values to +``$variable``. The order of items in the ``value`` array are +preserved. + +Examples: +^^^^^^^^^ + +$one_of_a_kind will be assigned ["a", "b"] + +:: + + ["unique", "$one_of_a_kind", ["a", "b", "a"]] + + +-------------------------------------------------------------------------------- + +regexp +------ + +``regexp string pattern`` + +string + The string the regular expression pattern is applied to. + +pattern + The regular expression pattern. + +**regexp** performs a regular expression match against ``string``. The +regular expression pattern syntax is defined by the regular expression +implementation of the language this API is written in. + +Pattern groups are a convenient way to select sub-matches. Pattern +groups may accessed by either group number or group name. After a +successful regular expression match the groups are stored in the +special variables ``$regexp_array`` and +``$regexp_map``. + +``$regexp_array`` is used to access the groups by +numerical index. Groups are numbered by counting the left parenthesis +group delimiter starting at 1. Group 0 is the entire +match. ``$regexp_array`` is valid irregardless of whether you used +named groups or not. + +``$regexp_map`` is used to access the groups by +name. ``$regexp_map`` is only valid if you used named groups in the +pattern. + +Examples: +^^^^^^^^^ + +Many user names are of the form "user@domain", to split the username +from the domain and to be able to work with those values independently +use a regular expression and then assign the results to a variable. In +this example there are two regular expression groups, the first group +is the username and the second group is the domain. In the first +example we use named groups and then access the match information in +the special variable ``$regexp_map`` via the name of the group. + +:: + + ["regexp", "$assertion[UserName]", "(?P\\w+)@(?P.+)"], + ["continue", "if_not_success"], + ["set", "$username", "$regexp_map[username]"], + ["set", "$domain", "$regexp_map[domain]"], + + +This is exactly equivalent but uses numbered groups instead of named +groups. In this instance the group matches are stored in the special +variable ``$regexp_array`` and accessed by numerical index. + +:: + + ["regexp", "$assertion[UserName]", "(\\w+)@(.+)"], + ["continue", "if_not_success"], + ["set", "$username", "$regexp_array[1]"], + ["set", "$domain", "$regexp_array[2]"], + + + +-------------------------------------------------------------------------------- + +regexp_replace +-------------- + +``regexp_replace $variable string pattern replacement`` + +$variable + The variable which receives result of the replacement. + +string + The string to perform the replacement on. + +pattern + The regular expression pattern. + +replacement + The replacement specification. + +**regexp_replace** replaces each occurrence of ``pattern`` in +``$string`` with ``replacement``. See `regexp`_ for details of using +regular expressions. + +Examples: +^^^^^^^^^ + +Convert hyphens in a name to underscores. + +:: + + ["regexp_replace", "$name", "$name", "-", "_"] + + +-------------------------------------------------------------------------------- + +split +----- + +``split $variable string pattern`` + +$variable + This variable is assigned an array containing the split items. + +string + The string to split into separate items. + +pattern + The regular expression pattern used to split the string. + +**split** splits ``string`` into separate pieces and assigns the +result to ``$variable`` as an array of pieces. The split occurs +wherever the regular expression ``pattern`` occurs in ``string``. See +`regexp`_ for details of using regular expressions. + +Examples: +^^^^^^^^^ + +Split a list of groups separated by a colon (:) into an array of +individual group names. If $assertion[Groups] contained the string +"user:admin" then $group_list will set to ["user", "admin"]. + +:: + + ["split", "$group_list", "$assertion[Groups]", ":"] + + + +-------------------------------------------------------------------------------- + +join +---- + +``join $variable array join_string`` + +$variable + This variable is assigned the string result of the join operation. + +array + An array of string items to be joined together with + ``$join_string``. + +join_string + The string inserted between each element in ``array``. + +**join** accepts an array of strings and produces a single string +where each element in the array is separated by ``join_string``. + +Examples: +^^^^^^^^^ + +Convert a list of group names into a single string where each group +name is separated by a colon (:). If the array ``$group_list`` is +["user", "admin"] and the ``join_string`` is ":" then the +``$group_string`` variable will be set to "user:admin". + +:: + + ["join", "$group_string", "$groups", ":"] + + +-------------------------------------------------------------------------------- + +lower +----- + +``lower $variable value`` + +$variable + This variable is assigned the result of the lower operation. + +value + The value to lower case, may be either a string, array, or + associative array. + +**lower** lower cases the input value. The input value may be one of +the following types: + +string + The string is lower cased. + +array + Each member of the array must be a string, the result is an array + with the items replaced by their lower case value. + +associative array + Each key in the associative array is lower cased. The values + associated with the key are **not** modified. + +Examples: +^^^^^^^^^ + +Lookup ``UserName`` in the assertion and set the variable +``$username`` to it's lower case value. + +:: + + ["lower", "$username", "$assertion[UserName]"], + +Set each member of the ``$groups`` array to it's lower case value. If +``$groups`` was ["User", "Admin"] then ``$groups`` will become +["user", "admin"]. + +:: + + ["lower", "$groups", "$groups"], + +To enable case insensitive lookup's in an associative array lower case +each key in the associative array. If ``$assertion`` was {"UserName": +"JoeUser"} then ``$assertion`` will become {"username": "JoeUser"} + +:: + + ["lower", "$assertion", $assertion"] + +-------------------------------------------------------------------------------- + +upper +----- + +``upper $variable value`` + +$variable + This variable is assigned the result of the upper operation. + +value + The value to upper case, may be either a string, array, or + associative array. + +**upper** is exactly analogous to `lower`_ except the values are upper +cased, see `lower`_ for details. + + +-------------------------------------------------------------------------------- + +in +-- + +``in member collection`` + +member + The value whose membership is being tested. + +collection + A collection of members. May be string, array or associative array. + +**in** tests to see if ``member`` is a member of ``collection``. The +membership test depends on the type of collection, the following are +supported: + +array + If any item in the array is equal to ``member`` then the result is + success. + +associative array + If the associative array contains a key equal to ``member`` then + the result is success. + +string + If the string contains a sub-string equal to ``member`` then the + result is success. + +Examples: +^^^^^^^^^ + +Test to see if the assertion contains a UserName value. + +:: + + ["in", "UserName", "$assertion"] + ["continue", "if_not_success"] + +Test to see if a group is one of "user" or "admin". + +:: + + ["in", "$group", ["user", "admin"]] + ["continue", "if_not_success"] + +Test to see if the sub-string "BigCorp" is in +the assertion's ``Provider`` value. + +:: + + ["in", "BigCorp", "$assertion[Provider]"] + ["continue", "if_not_success"] + + +-------------------------------------------------------------------------------- + +not_in +------ + +``in member collection`` + +member + The value whose membership is being tested. + +collection + A collection of members. May be string, array or associative array. + +**not_in** is exactly analogous to `in`_ except the sense of the test +is reversed. See `in`_ for details. + +-------------------------------------------------------------------------------- + +compare +------- + +``compare left operator right`` + +left + The left hand value of the binary operator. + +operator + The binary operator used for comparing left to right. + +right + The right hand value of the binary operator. + + +**compare** compares the left value to the right value according the +operator and sets success if the comparison evaluates to True. The +following relational operators are supported. + ++----------+-----------------------+ +| Operator | Description | ++==========+=======================+ +| == | equal | ++----------+-----------------------+ +| != | not equal | ++----------+-----------------------+ +| < | less than | ++----------+-----------------------+ +| <= | less than or equal | ++----------+-----------------------+ +| > | greater than | ++----------+-----------------------+ +| >= | greater than or equal | ++----------+-----------------------+ + + +The left and right hand sides of the comparison operator *must* be +the same type, no type conversions are performed. Not all combinations +of operator and type are supported. The table below illustrates the +supported combinations. Essentially you can test for equality or +inequality on any type. But only strings and numbers support the +magnitude relational operators. + + ++----------+--------+---------+------+---------+-----+------+------+ +| Operator | STRING | INTEGER | REAL | BOOLEAN | MAP | LIST | NULL | ++==========+========+=========+======+=========+=====+======+======+ +| == | X | X | X | X | X | X | X | ++----------+--------+---------+------+---------+-----+------+------+ +| != | X | X | X | X | X | X | X | ++----------+--------+---------+------+---------+-----+------+------+ +| < | X | X | X | | | | | ++----------+--------+---------+------+---------+-----+------+------+ +| <= | X | X | X | | | | | ++----------+--------+---------+------+---------+-----+------+------+ +| > | X | X | X | | | | | ++----------+--------+---------+------+---------+-----+------+------+ +| >= | X | X | X | | | | | ++----------+--------+---------+------+---------+-----+------+------+ + + +Examples: +^^^^^^^^^ + +Test to see if the ``$groups`` array has at least 2 members + +:: + + ["length", "$group_length", "$groups"], + ["compare", "$group_length", ">=", 2] + + +-------------------------------------------------------------------------------- + +exit +---- + +``exit status criteria`` + +status + The result for the rule. + +criteria + The criteria upon which will cause the rule will be immediately + exited with a failed status. + +**exit** causes the rule being executed to immediately exit and a rule +result if the specified criteria is met. Statement verbs such as `in`_ +or `compare`_ set the result status which may be tested with the +``success`` and ``not_success`` criteria. + +The exit ``status`` may be one of: + +rule_fails + The rule has failed and no mapping will occur. + +rule_succeeds + The rule succeeded and the mapping will be applied. + +The ``criteria`` may be one of: + +if_success + If current result status is success then exit with ``status``. + +if_not_success + If current result status is not success then exit with ``status``. + +always + Unconditionally exit with ``status``. + +never + Effectively a no-op. Useful for debugging. + +Examples: +^^^^^^^^^ + +The rule requires ``UserName`` to be in the assertion. + +:: + + ["in", "UserName", "$assertion"] + ["exit", "rule_fails", "if_not_success"] + +-------------------------------------------------------------------------------- + + +continue +-------- + +``continue criteria`` + +criteria + The criteria which causes the remainder of the *block* to be + skipped. + +**continue** is used to control execution for statement blocks. It +mirrors in a crude way the `if` expression in a procedural +language. ``continue`` does *not* affect the success or failure of a +rule, rather it controls whether subsequent statements in a block are +executed or not. Control continues at the next statement block. + +Statement verbs such as `in`_ or `compare`_ set the result status +which may be tested with the ``success`` and ``not_success`` criteria. + +The criteria may be one of: + +if_success + If current result status is success then exit the statement + block and continue execution at the next statement block. + +if_not_success + If current result status is not success then exit the statement + block and continue execution at the next statement block. + +always + Immediately exit the statement block and continue execution at the + next statement block. + +never + Effectively a no-op. Useful for debugging. Execution continues at + the next statement. + +Examples: +^^^^^^^^^ + +The following pseudo code: + +:: + + roles = []; + if ("Groups" in assertion) { + groups = assertion["Groups"].split(":"); + if ("qa_test" in groups) { + roles.append("tester"); + } + } + +could be implemented this way: + +:: + + [ + ["set", "$roles", []], + ["in", "Groups", "$assertion"], + ["continue", "if_not_success"], + ["split" "$groups", $assertion[Groups]", ":"], + ["in", "qa_test", "$groups"], + ["continue", "if_not_success"], + ["append", "$roles", "tester"] + ] diff --git a/java/src/main/java/com/redhat/IdPMapping/IdpJson.java b/java/src/main/java/com/redhat/IdPMapping/IdpJson.java new file mode 100644 index 0000000..7b95ea9 --- /dev/null +++ b/java/src/main/java/com/redhat/IdPMapping/IdpJson.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2014 Red Hat + * All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.idpmapping; + + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import javax.json.Json; +import javax.json.JsonValue; +import javax.json.stream.JsonGenerator; +import javax.json.stream.JsonGeneratorFactory; +import javax.json.stream.JsonLocation; +import javax.json.stream.JsonParser; +import javax.json.stream.JsonParser.Event; + + +/** + * Converts between JSON and the internal data structures used in the + * RuleProcessor. + * + * @author John Dennis + */ + +public class IdpJson { + + public IdpJson() {} + + public Object loadJson(java.io.Reader in) { + JsonParser parser = Json.createParser(in); + Event event = null; + + // Prime the pump. Get the first item from the parser. + event = parser.next(); + + // Act on first item. + return loadJsonItem(parser, event); + } + + public Object loadJson(Path filename) throws IOException { + BufferedReader reader = Files.newBufferedReader(filename, StandardCharsets.UTF_8); + return loadJson(reader); + } + + public Object loadJson(String string) { + StringReader reader = new StringReader(string); + return loadJson(reader); + } + + /* + * Process current parser item indicated by event. Consumes exactly the number of parser events + * necessary to load the item. Caller must advance the parser via parser.next() after this method + * returns. + */ + private Object loadJsonItem(JsonParser parser, Event event) { + switch (event) { + case START_OBJECT: { + return loadJsonObject(parser, event); + } + case START_ARRAY: { + return loadJsonArray(parser, event); + } + case VALUE_NULL: { + return null; + } + case VALUE_NUMBER: { + if (parser.isIntegralNumber()) { + return new Long(parser.getLong()); + } else { + return new Double(parser.getBigDecimal().doubleValue()); + } + } + case VALUE_STRING: { + return parser.getString(); + } + case VALUE_TRUE: { + return new Boolean(true); + } + case VALUE_FALSE: { + return new Boolean(false); + } + default: { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException(String.format( + "unknown JSON parsing event %s, location(line=%d column=%d offset=%d)", event, + location.getLineNumber(), location.getColumnNumber(), location.getStreamOffset())); + } + } + } + + private List loadJsonArray(JsonParser parser, Event event) { + List list = new ArrayList(); + + if (event != Event.START_ARRAY) { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException( + String + .format( + "expected JSON parsing event to be START_ARRAY, not %s location(line=%d column=%d offset=%d)", + event, location.getLineNumber(), location.getColumnNumber(), + location.getStreamOffset())); + } + event = parser.next(); // consume START_ARRAY + while (event != Event.END_ARRAY) { + Object obj; + + obj = loadJsonItem(parser, event); + list.add(obj); + event = parser.next(); // next array item or END_ARRAY + } + return list; + } + + private Map loadJsonObject(JsonParser parser, Event event) { + Map map = new LinkedHashMap(); + + if (event != Event.START_OBJECT) { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException(String.format( + "expected JSON parsing event to be START_OBJECT, not %s, ", + "location(line=%d column=%d offset=%d)", event, location.getLineNumber(), + location.getColumnNumber(), location.getStreamOffset())); + } + event = parser.next(); // consume START_OBJECT + while (event != Event.END_OBJECT) { + if (event == Event.KEY_NAME) { + String key; + Object value; + + key = parser.getString(); + event = parser.next(); // consume key + value = loadJsonItem(parser, event); + map.put(key, value); + } else { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException( + String + .format( + "expected JSON parsing event to be KEY_NAME, not %s, location(line=%d column=%d offset=%d)", + event, location.getLineNumber(), location.getColumnNumber(), + location.getStreamOffset())); + + } + event = parser.next(); // next key or END_OBJECT + } + return map; + } + + public String dumpJson(Object obj) { + Map properties = new HashMap(1); + properties.put(JsonGenerator.PRETTY_PRINTING, true); + JsonGeneratorFactory generatorFactory = Json.createGeneratorFactory(properties); + StringWriter stringWriter = new StringWriter(); + JsonGenerator generator = generatorFactory.createGenerator(stringWriter); + + dumpJsonItem(generator, obj); + generator.close(); + return stringWriter.toString(); + } + + private void dumpJsonItem(JsonGenerator generator, Object obj) { + // ordered by expected occurrence + if (obj instanceof String) { + generator.write((String) obj); + } else if (obj instanceof List) { + generator.writeStartArray(); + @SuppressWarnings("unchecked") + List list = (List) obj; + dumpJsonArray(generator, list); + } else if (obj instanceof Map) { + generator.writeStartObject(); + @SuppressWarnings("unchecked") + Map map = (Map) obj; + dumpJsonObject(generator, map); + } else if (obj instanceof Long) { + generator.write(((Long) obj).longValue()); + } else if (obj instanceof Boolean) { + generator.write(((Boolean) obj).booleanValue()); + } else if (obj == null) { + generator.writeNull(); + } else if (obj instanceof Double) { + generator.write(((Double) obj).doubleValue()); + } else { + throw new IllegalStateException( + String + .format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + + private void dumpJsonArray(JsonGenerator generator, List list) { + for (Object obj : list) { + dumpJsonItem(generator, obj); + } + generator.writeEnd(); + } + + private void dumpJsonObject(JsonGenerator generator, Map map) { + + + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object obj = entry.getValue(); + + // ordered by expected occurrence + if (obj instanceof String) { + generator.write(key, (String) obj); + } else if (obj instanceof List) { + generator.writeStartArray(key); + @SuppressWarnings("unchecked") + List list = (List) obj; + dumpJsonArray(generator, list); + } else if (obj instanceof Map) { + generator.writeStartObject(key); + @SuppressWarnings("unchecked") + Map map1 = (Map) obj; + dumpJsonObject(generator, map1); + } else if (obj instanceof Long) { + generator.write(key, ((Long) obj).longValue()); + } else if (obj instanceof Boolean) { + generator.write(key, ((Boolean) obj).booleanValue()); + } else if (obj == null) { + generator.write(key, JsonValue.NULL); + } else if (obj instanceof Double) { + generator.write(key, ((Double) obj).doubleValue()); + } else { + throw new IllegalStateException( + String + .format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + generator.writeEnd(); + } + +} diff --git a/java/src/main/java/com/redhat/IdPMapping/InvalidRuleException.java b/java/src/main/java/com/redhat/IdPMapping/InvalidRuleException.java new file mode 100644 index 0000000..61bf0c2 --- /dev/null +++ b/java/src/main/java/com/redhat/IdPMapping/InvalidRuleException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 Red Hat + * All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.idpmapping; + +/** + * Exception thrown when a mapping rule is improperly defined. + * + * @author John Dennis + */ + +public class InvalidRuleException extends RuntimeException { + + private static final long serialVersionUID = 1948891573270429630L; + + public InvalidRuleException() {} + + public InvalidRuleException(String message) { + super(message); + } + + public InvalidRuleException(Throwable cause) { + super(cause); + } + + public InvalidRuleException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/src/main/java/com/redhat/IdPMapping/InvalidTypeException.java b/java/src/main/java/com/redhat/IdPMapping/InvalidTypeException.java new file mode 100644 index 0000000..aea0416 --- /dev/null +++ b/java/src/main/java/com/redhat/IdPMapping/InvalidTypeException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 Red Hat + * All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.idpmapping; + +/** + * Exception thrown when the type of a value is incorrect for a given + * context. + * + * @author John Dennis + */ + +public class InvalidTypeException extends RuntimeException { + + private static final long serialVersionUID = 4437011247503994368L; + + public InvalidTypeException() {} + + public InvalidTypeException(String message) { + super(message); + } + + public InvalidTypeException(Throwable cause) { + super(cause); + } + + public InvalidTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/src/main/java/com/redhat/IdPMapping/InvalidValueException.java b/java/src/main/java/com/redhat/IdPMapping/InvalidValueException.java new file mode 100644 index 0000000..1524b4f --- /dev/null +++ b/java/src/main/java/com/redhat/IdPMapping/InvalidValueException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 Red Hat + * All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.idpmapping; + +/** + * Exception thrown when a value cannot be used in a given context. + * + * @author John Dennis + */ + +public class InvalidValueException extends RuntimeException { + + private static final long serialVersionUID = -2351651535772692180L; + + public InvalidValueException() {} + + public InvalidValueException(String message) { + super(message); + } + + public InvalidValueException(Throwable cause) { + super(cause); + } + + public InvalidValueException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/src/main/java/com/redhat/IdPMapping/RuleProcessor.java b/java/src/main/java/com/redhat/IdPMapping/RuleProcessor.java new file mode 100644 index 0000000..ebf4e0a --- /dev/null +++ b/java/src/main/java/com/redhat/IdPMapping/RuleProcessor.java @@ -0,0 +1,1382 @@ +/* + * Copyright (C) 2014 Red Hat + * All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.idpmapping; + + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.opendaylight.aaa.idpmapping.IdpJson; +import org.opendaylight.aaa.idpmapping.Token; + + + +enum ProcessResult { + RULE_FAIL, RULE_SUCCESS, BLOCK_CONTINUE, STATEMENT_CONTINUE +} + + +/** + * Evaluate a set of rules against an assertion from an external + * Identity Provider (IdP) mapping those assertion values to local + * values. + * + * @author John Dennis + */ + +public class RuleProcessor { + private static final Logger logger = LoggerFactory + .getLogger(RuleProcessor.class); + + public String ruleIdFormat = ""; + public String statementIdFormat = + ""; + + /* + * Reserved variables + */ + public static final String ASSERTION = "assertion"; + public static final String RULE_NUMBER = "rule_number"; + public static final String RULE_NAME = "rule_name"; + public static final String BLOCK_NUMBER = "block_number"; + public static final String BLOCK_NAME = "block_name"; + public static final String STATEMENT_NUMBER = "statement_number"; + public static final String REGEXP_ARRAY_VARIABLE = "regexp_array"; + public static final String REGEXP_MAP_VARIABLE = "regexp_map"; + + private static final String REGEXP_NAMED_GROUP_PAT = "\\(\\?<([a-zA-Z][a-zA-Z0-9]*)>"; + private static final Pattern REGEXP_NAMED_GROUP_RE = Pattern.compile(REGEXP_NAMED_GROUP_PAT); + + + List> rules = null; + boolean success = true; + Map> mappings = null; + + public RuleProcessor(java.io.Reader rulesIn, Map> mappings) { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List> loadJson = (List>) json.loadJson(rulesIn); + rules = loadJson; + } + + public RuleProcessor(Path rulesIn, Map> mappings) throws IOException { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List> loadJson = (List>) json.loadJson(rulesIn); + rules = loadJson; + } + + public RuleProcessor(String rulesIn, Map> mappings) { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List> loadJson = (List>) json.loadJson(rulesIn); + rules = loadJson; + } + + /* + * For some odd reason the Java Regular Expression API does not include a way to retrieve a map of + * the named groups and their values. The API only permits us to retrieve a named group if we + * already know the group names. So instead we parse the pattern string looking for named groups, + * extract the name, look up the value of the named group and build a map from that. + */ + + private Map regexpGroupMap(String pattern, Matcher matcher) { + Map groupMap = new HashMap(); + Matcher groupMatcher = REGEXP_NAMED_GROUP_RE.matcher(pattern); + + while (groupMatcher.find()) { + String groupName = groupMatcher.group(1); + + groupMap.put(groupName, matcher.group(groupName)); + } + return groupMap; + } + + static public String join(List list, String conjunction) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Object item : list) { + if (first) { + first = false; + } else { + sb.append(conjunction); + } + sb.append(item.toString()); + } + return sb.toString(); + } + + private List regexpGroupList(Matcher matcher) { + List groupList = new ArrayList(matcher.groupCount() + 1); + groupList.add(0, matcher.group(0)); + for (int i = 1; i < matcher.groupCount() + 1; i++) { + groupList.add(i, matcher.group(i)); + } + return groupList; + } + + private String objToString(Object obj) { + StringWriter sw = new StringWriter(); + objToStringItem(sw, obj); + return sw.toString(); + } + + private void objToStringItem(StringWriter sw, Object obj) { + // ordered by expected occurrence + if (obj instanceof String) { + sw.write('"'); + sw.write(((String) obj).replaceAll("\"", "\\\"")); + sw.write('"'); + } else if (obj instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) obj; + boolean first = true; + + sw.write('['); + for (Object item : list) { + if (first) { + first = false; + } else { + sw.write(", "); + } + objToStringItem(sw, item); + } + sw.write(']'); + } else if (obj instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) obj; + boolean first = true; + + sw.write('{'); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (first) { + first = false; + } else { + sw.write(", "); + } + + objToStringItem(sw, key); + sw.write(": "); + objToStringItem(sw, value); + + } + sw.write('}'); + } else if (obj instanceof Long) { + sw.write(((Long) obj).toString()); + } else if (obj instanceof Boolean) { + sw.write(((Boolean) obj).toString()); + } else if (obj == null) { + sw.write("null"); + } else if (obj instanceof Double) { + sw.write(((Double) obj).toString()); + } else { + throw new IllegalStateException( + String + .format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + + private Object deepCopy(Object obj) { + // ordered by expected occurrence + if (obj instanceof String) { + return obj; // immutable + } else if (obj instanceof List) { + List new_list = new ArrayList(); + @SuppressWarnings("unchecked") + List list = (List) obj; + for (Object item : list) { + new_list.add(deepCopy(item)); + } + return new_list; + } else if (obj instanceof Map) { + Map new_map = new LinkedHashMap(); + @SuppressWarnings("unchecked") + Map map = (Map) obj; + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); // immutable + Object value = entry.getValue(); + new_map.put(key, deepCopy(value)); + } + return new_map; + } else if (obj instanceof Long) { + return obj; // immutable + } else if (obj instanceof Boolean) { + return obj; // immutable + } else if (obj == null) { + return null; + } else if (obj instanceof Double) { + return obj; // immutable + } else { + throw new IllegalStateException( + String + .format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + + public String ruleId(Map namespace) { + return substituteVariables(ruleIdFormat, namespace); + } + + public String statementId(Map namespace) { + return substituteVariables(statementIdFormat, namespace); + } + + public String substituteVariables(String string, Map namespace) + { + StringBuffer sb = new StringBuffer(); + Matcher matcher = Token.VARIABLE_RE.matcher(string); + + while (matcher.find()) { + Token token = new Token(matcher.group(0), namespace); + token.load(); + String replacement; + if (token.type == TokenType.STRING) { + replacement = token.getStringValue(); + } else { + replacement = objToString(token.getObjectValue()); + } + + matcher.appendReplacement(sb, replacement); + } + matcher.appendTail(sb); + return sb.toString(); + } + + Map getMapping(Map namespace, Map rule) + { + Map mapping = null; + String mappingName = null; + + try { + @SuppressWarnings("unchecked") + Map map = (Map) rule.get("mapping"); + mapping = map; + } catch (java.lang.ClassCastException e) { + throw new InvalidRuleException(String.format("%s rule defines 'mapping' but it is not a Map", + this.ruleId(namespace))); + } + if (mapping != null) { + return mapping; + } + try { + mappingName = (String) rule.get("mapping_name"); + } catch (java.lang.ClassCastException e) { + throw new InvalidRuleException(String.format( + "%s rule defines 'mapping_name' but it is not a string", this.ruleId(namespace))); + } + if (mappingName == null) { + throw new InvalidRuleException(String.format( + "%s rule does not define mapping nor mapping_name unable to load mapping", + this.ruleId(namespace))); + } + mapping = this.mappings.get(mappingName); + if (mapping == null) { + throw new InvalidRuleException( + String + .format( + "%s rule specifies mapping_name '%s' but a mapping by that name does not exist, unable to load mapping", + this.ruleId(namespace))); + } + logger.debug(String.format("using named mapping '%s' from rule %s mapping=%s", mappingName, + this.ruleId(namespace), mapping)); + return mapping; + } + + private String getVerb(List statement) { + Token verb; + + if (statement.size() < 1) { + throw new InvalidRuleException("statement has no verb"); + } + + try { + verb = new Token(statement.get(0), null); + } catch (Exception e) { + throw new InvalidRuleException( + String.format("statement first member (i.e. verb) error %s", e)); + } + + if (verb.type != TokenType.STRING) { + throw new InvalidRuleException(String.format( + "statement first member (i.e. verb) must be a string, not %s", verb.type)); + } + + return (verb.getStringValue()).toLowerCase(); + } + + private Token getToken(String verb, List statement, int index, + Map namespace, Set storageTypes, Set tokenTypes) { + Object item; + Token token; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, index + 1, + statement.size())); + } + + try { + token = new Token(item, namespace); + } catch (Exception e) { + throw new StatementErrorException(String.format("parameter %d, %s", index, e)); + } + + if (storageTypes != null) { + if (!storageTypes.contains(token.storageType)) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #%d to have storage types %s not %s. statement=%s", verb, + index, storageTypes, statement)); + } + } + + if (tokenTypes != null) { + token.load(); // Note, Token.load() sets the Token.type + + if (!tokenTypes.contains(token.type)) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #%d to have types %s, not %s. statement=%s", verb, index, + tokenTypes, statement)); + } + } + + return token; + } + + private Token getParameter(String verb, List statement, int index, + Map namespace, Set tokenTypes) { + Object item; + Token token; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, index + 1, + statement.size())); + } + + try { + token = new Token(item, namespace); + } catch (Exception e) { + throw new StatementErrorException(String.format("parameter %d, %s", index, e)); + } + + token.load(); + + if (tokenTypes != null) { + try { + token.get(); // Note, Token.get() sets the Token.type + } catch (UndefinedValueException e) { + // OK if not yet defined + } + if (!tokenTypes.contains(token.type)) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #%d to have types %s, not %s. statement=%s", verb, index, + tokenTypes, item.getClass().getSimpleName(), statement)); + } + } + + return token; + } + + private Object getRawParameter(String verb, List statement, int index, + Set tokenTypes) { + Object item; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, index + 1, + statement.size())); + } + + if (tokenTypes != null) { + TokenType itemType = Token.classify(item); + + if (!tokenTypes.contains(itemType)) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #%d to have types %s, not %s. statement=%s", verb, index, + tokenTypes, statement)); + } + } + + return item; + } + + private Token getVariable(String verb, List statement, int index, + Map namespace) { + Object item; + Token token; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, index + 1, + statement.size())); + } + + try { + token = new Token(item, namespace); + } catch (Exception e) { + throw new StatementErrorException(String.format("parameter %d, %s", index, e)); + } + + if (token.storageType != TokenStorageType.VARIABLE) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #%d to be a variable not %s. statement=%s", verb, index, + token.storageType, statement)); + } + + return token; + } + + public Map process(String assertionJson) { + ProcessResult result; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + Map assertion = (Map) json.loadJson(assertionJson); + System.out.println(assertionJson); + System.out.println(json.dumpJson(assertion)); + this.success = true; + + for (int ruleNumber = 0; ruleNumber < this.rules.size(); ruleNumber++) { + Map namespace = new HashMap(); + Map rule = (Map) this.rules.get(ruleNumber); + namespace.put(RULE_NUMBER, new Long(ruleNumber)); + namespace.put(RULE_NAME, new String("")); + namespace.put(ASSERTION, deepCopy(assertion)); + + result = processRule(namespace, rule); + + if (result == ProcessResult.RULE_SUCCESS) { + Map mapped = new LinkedHashMap(); + Map mapping = getMapping(namespace, rule); + for (Map.Entry entry : ((Map) mapping).entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + Object newValue = null; + try { + Token token = new Token(value, namespace); + newValue = token.get(); + } catch (Exception e) { + throw new InvalidRuleException(String.format( + "%s unable to get value for mapping %s=%s, %s", ruleId(namespace), key, value, e), + e); + } + mapped.put(key, newValue); + } + return mapped; + } + } + return null; + } + + private ProcessResult processRule(Map namespace, Map rule) + { + ProcessResult result = ProcessResult.BLOCK_CONTINUE; + @SuppressWarnings("unchecked") + List>> statementBlocks = + (List>>) rule.get("statement_blocks"); + if (statementBlocks == null) { + throw new InvalidRuleException("rule missing 'statement_blocks'"); + + } + for (int blockNumber = 0; blockNumber < statementBlocks.size(); blockNumber++) { + List> block = (List>) statementBlocks.get(blockNumber); + namespace.put(BLOCK_NUMBER, new Long(blockNumber)); + namespace.put(BLOCK_NAME, ""); + + result = processBlock(namespace, block); + System.out.println(); + if (EnumSet.of(ProcessResult.RULE_SUCCESS, ProcessResult.RULE_FAIL).contains(result)) { + break; + } else if (result == ProcessResult.BLOCK_CONTINUE) { + continue; + } else { + throw new IllegalStateException(String.format("%s unexpected statement result: %s", result)); + } + } + if (EnumSet.of(ProcessResult.RULE_SUCCESS, ProcessResult.BLOCK_CONTINUE).contains(result)) { + return ProcessResult.RULE_SUCCESS; + } else { + return ProcessResult.RULE_FAIL; + } + } + + private ProcessResult processBlock(Map namespace, List> block) + { + ProcessResult result = ProcessResult.STATEMENT_CONTINUE; + + for (int statementNumber = 0; statementNumber < block.size(); statementNumber++) { + List statement = (List) block.get(statementNumber); + namespace.put(STATEMENT_NUMBER, new Long(statementNumber)); + + try { + result = processStatement(namespace, statement); + } catch (Exception e) { + throw new IllegalStateException(String.format("%s statement=%s %s", statementId(namespace), + statement, e), e); + } + if (EnumSet.of(ProcessResult.BLOCK_CONTINUE, ProcessResult.RULE_SUCCESS, + ProcessResult.RULE_FAIL).contains(result)) { + break; + } else if (result == ProcessResult.STATEMENT_CONTINUE) { + continue; + } else { + throw new IllegalStateException(String.format("%s unexpected statement result: %s", result)); + } + } + if (result == ProcessResult.STATEMENT_CONTINUE) { + result = ProcessResult.BLOCK_CONTINUE; + } + return result; + } + + private ProcessResult processStatement(Map namespace, List statement) + { + ProcessResult result = ProcessResult.STATEMENT_CONTINUE; + String verb = getVerb(statement); + + switch (verb) { + case "set": + result = verbSet(verb, namespace, statement); + break; + case "length": + result = verbLength(verb, namespace, statement); + break; + case "interpolate": + result = verbInterpolate(verb, namespace, statement); + break; + case "append": + result = verbAppend(verb, namespace, statement); + break; + case "unique": + result = verbUnique(verb, namespace, statement); + break; + case "split": + result = verbSplit(verb, namespace, statement); + break; + case "join": + result = verbJoin(verb, namespace, statement); + break; + case "lower": + result = verbLower(verb, namespace, statement); + break; + case "upper": + result = verbUpper(verb, namespace, statement); + break; + case "in": + result = verbIn(verb, namespace, statement); + break; + case "not_in": + result = verbNotIn(verb, namespace, statement); + break; + case "compare": + result = verbCompare(verb, namespace, statement); + break; + case "regexp": + result = verbRegexp(verb, namespace, statement); + break; + case "regexp_replace": + result = verbRegexpReplace(verb, namespace, statement); + break; + case "exit": + result = verbExit(verb, namespace, statement); + break; + case "continue": + result = verbContinue(verb, namespace, statement); + break; + default: + throw new InvalidRuleException(String.format("unknown verb '%s'", verb)); + } + + return result; + } + + private ProcessResult verbSet(String verb, Map namespace, List statement) + { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = getParameter(verb, statement, 2, namespace, null); + + variable.set(parameter.getObjectValue()); + this.success = true; + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s variable: %s=%s", statementId(namespace), + verb, this.success, variable, variable.get())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbLength(String verb, Map namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = + getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.ARRAY, TokenType.MAP, TokenType.STRING)); + long length; + + + switch (parameter.type) { + case ARRAY: { + length = parameter.getListValue().size(); + } + break; + case MAP: { + length = parameter.getMapValue().size(); + } + break; + case STRING: { + length = parameter.getStringValue().length(); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", parameter.type)); + } + + variable.set(new Long(length)); + this.success = true; + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s variable: %s=%s parameter=%s", + statementId(namespace), verb, this.success, variable, variable.get(), + parameter.getObjectValue())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbInterpolate(String verb, Map namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + String string = (String) getRawParameter(verb, statement, 2, EnumSet.of(TokenType.STRING)); + String newValue = null; + + try { + newValue = substituteVariables(string, namespace); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' string='%s': %s", verb, variable, string, e)); + } + variable.set(newValue); + this.success = true; + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s variable: %s=%s string='%s'", + statementId(namespace), verb, this.success, variable, variable.get(), string)); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbAppend(String verb, Map namespace, + List statement) { + Token variable = + getToken(verb, statement, 1, namespace, EnumSet.of(TokenStorageType.VARIABLE), + EnumSet.of(TokenType.ARRAY)); + Token item = getParameter(verb, statement, 2, namespace, null); + + try { + List list = variable.getListValue(); + list.add(item.getObjectValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' item='%s': %s", verb, variable.getObjectValue(), + item.getObjectValue(), e)); + } + this.success = true; + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s variable: %s=%s item=%s", + statementId(namespace), verb, this.success, variable, variable.get(), + item.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbUnique(String verb, Map namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token array = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.ARRAY)); + + List newValue = new ArrayList(); + Set seen = new HashSet(); + + for (Object member : array.getListValue()) { + if (seen.contains(member)) { + continue; + } else { + newValue.add(member); + seen.add(member); + } + } + + variable.set(newValue); + this.success = true; + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s variable: %s=%s array=%s", + statementId(namespace), verb, this.success, variable, variable.get(), + array.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbSplit(String verb, Map namespace, List statement) + { + Token variable = getVariable(verb, statement, 1, namespace); + Token string = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + Token pattern = getParameter(verb, statement, 3, namespace, EnumSet.of(TokenType.STRING)); + + Pattern regexp; + List newValue; + + try { + regexp = Pattern.compile(pattern.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, bad regular expression pattern '%s', %s", verb, + pattern.getObjectValue(), e)); + } + try { + newValue = + new ArrayList(Arrays.asList(regexp.split((String) string.getStringValue()))); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, string='%s' pattern='%s', %s", verb, string.getObjectValue(), + pattern.getObjectValue(), e)); + } + + variable.set(newValue); + this.success = true; + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s variable: %s=%s string='%s' pattern='%s'", + statementId(namespace), verb, this.success, variable, variable.get(), + string.getObjectValue(), pattern.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbJoin(String verb, Map namespace, List statement) + { + Token variable = getVariable(verb, statement, 1, namespace); + Token array = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.ARRAY)); + Token conjunction = getParameter(verb, statement, 3, namespace, EnumSet.of(TokenType.STRING)); + String newValue; + + try { + newValue = join(array.getListValue(), conjunction.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, array=%s conjunction='%s', %s", verb, array.getObjectValue(), + conjunction.getObjectValue(), e)); + } + + variable.set(newValue); + this.success = true; + + if (logger.isDebugEnabled()) { + logger.debug(String.format( + "%s verb='%s' success=%s variable: %s=%s array='%s' conjunction='%s'", + statementId(namespace), verb, this.success, variable, variable.get(), + array.getObjectValue(), conjunction.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbLower(String verb, Map namespace, List statement) + { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = + getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.STRING, TokenType.ARRAY, TokenType.MAP)); + + try { + switch (parameter.type) { + case STRING: { + String oldValue = parameter.getStringValue(); + String newValue; + newValue = oldValue.toLowerCase(); + variable.set(newValue); + } + break; + case ARRAY: { + List oldValue = parameter.getListValue(); + List newValue = new ArrayList(oldValue.size()); + String oldItem; + String newItem; + + for (Object item : oldValue) { + try { + oldItem = (String) item; + } catch (ClassCastException e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, array item (%s) is not a string, array=%s", verb, item, + parameter.getObjectValue())); + } + newItem = oldItem.toLowerCase(); + newValue.add(newItem); + } + variable.set(newValue); + } + break; + case MAP: { + Map oldValue = parameter.getMapValue(); + Map newValue = new LinkedHashMap(oldValue.size()); + + for (Map.Entry entry : oldValue.entrySet()) { + String oldKey; + String newKey; + Object value = entry.getValue(); + + oldKey = entry.getKey(); + newKey = oldKey.toLowerCase(); + newValue.put(newKey, value); + } + variable.set(newValue); + } + break; + default: + throw new IllegalStateException( + String.format("unexpected token type: %s", parameter.type)); + } + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' parameter='%s': %s", verb, variable, + parameter.getObjectValue(), e), e); + } + this.success = true; + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s variable: %s=%s parameter=%s", + statementId(namespace), verb, this.success, variable, variable.get(), + parameter.getObjectValue())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbUpper(String verb, Map namespace, List statement) + { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = + getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.STRING, TokenType.ARRAY, TokenType.MAP)); + + try { + switch (parameter.type) { + case STRING: { + String oldValue = parameter.getStringValue(); + String newValue; + newValue = oldValue.toUpperCase(); + variable.set(newValue); + } + break; + case ARRAY: { + List oldValue = parameter.getListValue(); + List newValue = new ArrayList(oldValue.size()); + String oldItem; + String newItem; + + for (Object item : oldValue) { + try { + oldItem = (String) item; + } catch (ClassCastException e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, array item (%s) is not a string, array=%s", verb, item, + parameter.getObjectValue())); + } + newItem = oldItem.toUpperCase(); + newValue.add(newItem); + } + variable.set(newValue); + } + break; + case MAP: { + Map oldValue = parameter.getMapValue(); + Map newValue = new LinkedHashMap(oldValue.size()); + + for (Map.Entry entry : oldValue.entrySet()) { + String oldKey; + String newKey; + Object value = entry.getValue(); + + oldKey = entry.getKey(); + newKey = oldKey.toUpperCase(); + newValue.put(newKey, value); + } + variable.set(newValue); + } + break; + default: + throw new IllegalStateException( + String.format("unexpected token type: %s", parameter.type)); + } + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' parameter='%s': %s", verb, variable, + parameter.getObjectValue(), e), e); + } + this.success = true; + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s variable: %s=%s parameter=%s", + statementId(namespace), verb, this.success, variable, variable.get(), + parameter.getObjectValue())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbIn(String verb, Map namespace, List statement) + { + Token member = getParameter(verb, statement, 1, namespace, null); + Token collection = + getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.ARRAY, TokenType.MAP, TokenType.STRING)); + + switch (collection.type) { + case ARRAY: { + this.success = collection.getListValue().contains(member.getObjectValue()); + } + break; + case MAP: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = collection.getMapValue().containsKey(member.getObjectValue()); + } + break; + case STRING: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = (collection.getStringValue()).contains(member.getStringValue()); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", collection.type)); + } + + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s member=%s collection=%s", + statementId(namespace), verb, this.success, member.getObjectValue(), + collection.getObjectValue())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbNotIn(String verb, Map namespace, List statement) + { + Token member = getParameter(verb, statement, 1, namespace, null); + Token collection = + getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.ARRAY, TokenType.MAP, TokenType.STRING)); + + switch (collection.type) { + case ARRAY: { + this.success = !collection.getListValue().contains(member.getObjectValue()); + } + break; + case MAP: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = !collection.getMapValue().containsKey(member.getObjectValue()); + } + break; + case STRING: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = !(collection.getStringValue()).contains(member.getStringValue()); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", collection.type)); + } + + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s member=%s collection=%s", + statementId(namespace), verb, this.success, member.getObjectValue(), + collection.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbCompare(String verb, Map namespace, + List statement) { + Token left = getParameter(verb, statement, 1, namespace, null); + Token op = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + Token right = getParameter(verb, statement, 3, namespace, null); + String invalidOp = "operator %s not supported for type %s"; + TokenType tokenType; + String opValue = op.getStringValue(); + boolean result; + + if (left.type != right.type) { + throw new InvalidTypeException(String.format( + "verb '%s' both items must have the same type left is %s and right is %s", verb, + left.type, right.type)); + } else { + tokenType = left.type; + } + + switch (opValue) { + case "==": + case "!=": { + switch (tokenType) { + case STRING: { + String leftValue = left.getStringValue(); + String rightValue = right.getStringValue(); + result = leftValue.equals(rightValue); + } + break; + case INTEGER: { + Long leftValue = left.getLongValue(); + Long rightValue = right.getLongValue(); + result = leftValue.equals(rightValue); + } + break; + case REAL: { + Double leftValue = left.getDoubleValue(); + Double rightValue = right.getDoubleValue(); + result = leftValue.equals(rightValue); + } + break; + case ARRAY: { + List leftValue = left.getListValue(); + List rightValue = right.getListValue(); + result = leftValue.equals(rightValue); + } + break; + case MAP: { + Map leftValue = left.getMapValue(); + Map rightValue = right.getMapValue(); + result = leftValue.equals(rightValue); + } + break; + case BOOLEAN: { + Boolean leftValue = left.getBooleanValue(); + Boolean rightValue = right.getBooleanValue(); + result = leftValue.equals(rightValue); + } + break; + case NULL: { + result = (left.getNullValue() == right.getNullValue()); + } + break; + default: { + throw new IllegalStateException(String.format("unexpected token type: %s", tokenType)); + } + } + if (opValue.equals("!=")) { // negate the sense of the test + result = !result; + } + } + break; + case "<": + case ">=": { + switch (tokenType) { + case STRING: { + String leftValue = left.getStringValue(); + String rightValue = right.getStringValue(); + result = leftValue.compareTo(rightValue) < 0; + } + break; + case INTEGER: { + Long leftValue = left.getLongValue(); + Long rightValue = right.getLongValue(); + result = leftValue < rightValue; + } + break; + case REAL: { + Double leftValue = left.getDoubleValue(); + Double rightValue = right.getDoubleValue(); + result = leftValue < rightValue; + } + break; + case ARRAY: + case MAP: + case BOOLEAN: + case NULL: { + throw new InvalidRuleException(String.format(invalidOp, opValue, tokenType)); + } + default: { + throw new IllegalStateException(String.format("unexpected token type: %s", tokenType)); + } + } + if (opValue.equals(">=")) { // negate the sense of the test + result = !result; + } + } + break; + case ">": + case "<=": { + switch (tokenType) { + case STRING: { + String leftValue = left.getStringValue(); + String rightValue = right.getStringValue(); + result = leftValue.compareTo(rightValue) > 0; + } + break; + case INTEGER: { + Long leftValue = left.getLongValue(); + Long rightValue = right.getLongValue(); + result = leftValue > rightValue; + } + break; + case REAL: { + Double leftValue = left.getDoubleValue(); + Double rightValue = right.getDoubleValue(); + result = leftValue > rightValue; + } + break; + case ARRAY: + case MAP: + case BOOLEAN: + case NULL: { + throw new InvalidRuleException(String.format(invalidOp, opValue, tokenType)); + } + default: { + throw new IllegalStateException(String.format("unexpected token type: %s", tokenType)); + } + } + if (opValue.equals("<=")) { // negate the sense of the test + result = !result; + } + } + break; + default: { + throw new InvalidRuleException(String.format( + "verb '%s' has unknown comparison operator '%s'", verb, op.getObjectValue())); + } + } + this.success = result; + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s left=%s op='%s' right=%s", + statementId(namespace), verb, this.success, left.getObjectValue(), op.getObjectValue(), + right.getObjectValue())); + } + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbRegexp(String verb, Map namespace, + List statement) { + Token string = getParameter(verb, statement, 1, namespace, EnumSet.of(TokenType.STRING)); + Token pattern = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + + Pattern regexp; + Matcher matcher; + + try { + regexp = Pattern.compile(pattern.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, bad regular expression pattern '%s', %s", verb, + pattern.getObjectValue(), e)); + } + matcher = regexp.matcher(string.getStringValue()); + + if (matcher.find()) { + this.success = true; + namespace.put(REGEXP_ARRAY_VARIABLE, regexpGroupList(matcher)); + namespace.put(REGEXP_MAP_VARIABLE, regexpGroupMap(pattern.getStringValue(), matcher)); + } else { + this.success = false; + namespace.put(REGEXP_ARRAY_VARIABLE, new ArrayList()); + namespace.put(REGEXP_MAP_VARIABLE, new HashMap()); + } + + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s string='%s' pattern='%s' %s=%s %s=%s", + statementId(namespace), verb, this.success, string.getObjectValue(), + pattern.getObjectValue(), REGEXP_ARRAY_VARIABLE, namespace.get(REGEXP_ARRAY_VARIABLE), + REGEXP_MAP_VARIABLE, namespace.get(REGEXP_MAP_VARIABLE))); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbRegexpReplace(String verb, Map namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token string = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + Token pattern = getParameter(verb, statement, 3, namespace, EnumSet.of(TokenType.STRING)); + Token replacement = getParameter(verb, statement, 4, namespace, EnumSet.of(TokenType.STRING)); + + Pattern regexp; + Matcher matcher; + String newValue; + + try { + regexp = Pattern.compile(pattern.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, bad regular expression pattern '%s', %s", verb, + pattern.getObjectValue(), e)); + } + matcher = regexp.matcher(string.getStringValue()); + + newValue = matcher.replaceAll(replacement.getStringValue()); + variable.set(newValue); + this.success = true; + + if (logger.isDebugEnabled()) { + logger.debug(String.format( + "%s verb='%s' success=%s variable: %s=%s string='%s' pattern='%s' replacement='%s'", + statementId(namespace), verb, this.success, variable, variable.get(), + string.getObjectValue(), pattern.getObjectValue(), replacement.getObjectValue())); + } + + return ProcessResult.STATEMENT_CONTINUE; + } + + private ProcessResult verbExit(String verb, Map namespace, List statement) + { + ProcessResult statementResult = ProcessResult.STATEMENT_CONTINUE; + + Token exitStatusParam = + getParameter(verb, statement, 1, namespace, EnumSet.of(TokenType.STRING)); + Token criteriaParam = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + String exitStatus = (exitStatusParam.getStringValue()).toLowerCase(); + String criteria = (criteriaParam.getStringValue()).toLowerCase(); + ProcessResult result; + boolean doExit; + + + if (exitStatus.equals("rule_succeeds")) { + result = ProcessResult.RULE_SUCCESS; + } else if (exitStatus.equals("rule_fails")) { + result = ProcessResult.RULE_FAIL; + } else { + throw new InvalidRuleException(String.format("verb='%s' unknown exit status '%s'", verb, + exitStatus)); + } + + + if (criteria.equals("if_success")) { + if (this.success) { + doExit = true; + } else { + doExit = false; + } + } else if (criteria.equals("if_not_success")) { + if (!this.success) { + doExit = true; + } else { + doExit = false; + } + } else if (criteria.equals("always")) { + doExit = true; + } else if (criteria.equals("never")) { + doExit = false; + } else { + throw new InvalidRuleException(String.format("verb='%s' unknown exit criteria '%s'", verb, + criteria)); + } + + if (doExit) { + statementResult = result; + } + + if (logger.isDebugEnabled()) { + logger.debug(String + .format("%s verb='%s' success=%s status=%s criteria=%s exiting=%s result=%s", + statementId(namespace), verb, this.success, exitStatus, criteria, doExit, + statementResult)); + } + + return statementResult; + } + + private ProcessResult verbContinue(String verb, Map namespace, + List statement) { + ProcessResult statementResult = ProcessResult.STATEMENT_CONTINUE; + Token criteriaParam = getParameter(verb, statement, 1, namespace, EnumSet.of(TokenType.STRING)); + String criteria = (criteriaParam.getStringValue()).toLowerCase(); + boolean doContinue; + + if (criteria.equals("if_success")) { + if (this.success) { + doContinue = true; + } else { + doContinue = false; + } + } else if (criteria.equals("if_not_success")) { + if (!this.success) { + doContinue = true; + } else { + doContinue = false; + } + } else if (criteria.equals("always")) { + doContinue = true; + } else if (criteria.equals("never")) { + doContinue = false; + } else { + throw new InvalidRuleException(String.format("verb='%s' unknown continue criteria '%s'", + verb, criteria)); + } + + if (doContinue) { + statementResult = ProcessResult.BLOCK_CONTINUE; + } + + if (logger.isDebugEnabled()) { + logger.debug(String.format("%s verb='%s' success=%s criteria=%s continuing=%s result=%s", + statementId(namespace), verb, this.success, criteria, doContinue, statementResult)); + } + + return statementResult; + } + +} diff --git a/java/src/main/java/com/redhat/IdPMapping/StatementErrorException.java b/java/src/main/java/com/redhat/IdPMapping/StatementErrorException.java new file mode 100644 index 0000000..4d17f64 --- /dev/null +++ b/java/src/main/java/com/redhat/IdPMapping/StatementErrorException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 Red Hat + * All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.idpmapping; + +/** + * Exception thrown when a mapping rule statement fails. + * + * @author John Dennis + */ + +public class StatementErrorException extends RuntimeException { + + private static final long serialVersionUID = 8312665727576018327L; + + public StatementErrorException() {} + + public StatementErrorException(String message) { + super(message); + } + + public StatementErrorException(Throwable cause) { + super(cause); + } + + public StatementErrorException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/src/main/java/com/redhat/IdPMapping/Token.java b/java/src/main/java/com/redhat/IdPMapping/Token.java new file mode 100644 index 0000000..9b835cb --- /dev/null +++ b/java/src/main/java/com/redhat/IdPMapping/Token.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2014 Red Hat + * All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.idpmapping; + + + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +enum TokenStorageType { + UNKNOWN, CONSTANT, VARIABLE +} + + +enum TokenType { + STRING, // java String + ARRAY, // java List + MAP, // java Map + INTEGER, // java Long + BOOLEAN, // java Boolean + NULL, // java null + REAL, // java Double + UNKNOWN, // undefined +} + + +/** + * Rule statements can contain variables or constants, this class + * encapsulates those values, enforces type handling and supports + * reading and writing of those values. + * + * Technically at the syntactic level these are not tokens. A token + * would have finer granularity such as identifier, operator, etc. I + * just couldn't think of a better name for how they're used here and + * thought token was a reasonable compromise as a name. + * + * @author John Dennis + */ + +class Token { + + /* + * Regexp to identify a variable beginning with $ Supports array notation, e.g. $foo[bar] Optional + * delimiting braces may be used to separate variable from surrounding text. + * + * Examples: $foo ${foo} $foo[bar] ${foo[bar] where foo is the variable name and bar is the array + * index. + * + * Identifer is any alphabetic followed by alphanumeric or underscore + */ + private static final String VARIABLE_PAT = "(? namespace = null; + public TokenStorageType storageType = TokenStorageType.UNKNOWN; + public TokenType type = TokenType.UNKNOWN; + public String name = null; + public String index = null; + + Token(Object input, Map namespace) { + this.namespace = namespace; + if (input instanceof String) { + parseVariable((String) input); + if (this.storageType == TokenStorageType.CONSTANT) { + this.value = input; + this.type = classify(input); + } + } else { + this.storageType = TokenStorageType.CONSTANT; + this.value = input; + this.type = classify(input); + } + } + + @Override + public String toString() { + if (this.storageType == TokenStorageType.CONSTANT) { + return String.format("%s", this.value); + } else if (this.storageType == TokenStorageType.VARIABLE) { + if (this.index == null) { + return String.format("$%s", this.name); + } else { + return String.format("$%s[%s]", this.name, this.index); + } + } else { + return "UNKNOWN"; + } + } + + void parseVariable(String string) { + Matcher matcher = VARIABLE_ONLY_RE.matcher(string); + if (matcher.find()) { + String name = matcher.group(1); + String index = matcher.group(3); + + this.storageType = TokenStorageType.VARIABLE; + this.name = name; + this.index = index; + } else { + this.storageType = TokenStorageType.CONSTANT; + } + } + + public static TokenType classify(Object value) { + TokenType tokenType = TokenType.UNKNOWN; + // ordered by expected occurrence + if (value instanceof String) { + tokenType = TokenType.STRING; + } else if (value instanceof List) { + tokenType = TokenType.ARRAY; + } else if (value instanceof Map) { + tokenType = TokenType.MAP; + } else if (value instanceof Long) { + tokenType = TokenType.INTEGER; + } else if (value instanceof Boolean) { + tokenType = TokenType.BOOLEAN; + } else if (value == null) { + tokenType = TokenType.NULL; + } else if (value instanceof Double) { + tokenType = TokenType.REAL; + } else { + throw new InvalidRuleException(String.format( + "Type must be String, Long, Double, Boolean, List, Map, or null, not %s", value + .getClass().getSimpleName(), value)); + } + return tokenType; + } + + Object get() { + return get(null); + } + + Object get(Object index) { + Object base = null; + + if (this.storageType == TokenStorageType.CONSTANT) { + return this.value; + } + + if (this.namespace.containsKey(this.name)) { + base = this.namespace.get(this.name); + } else { + throw new UndefinedValueException(String.format("variable '%s' not defined", this.name)); + } + + if (index == null) { + index = this.index; + } + + if (index == null) { // scalar types + value = base; + } else { + if (base instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) base; + Integer idx = null; + + if (index instanceof Long) { + idx = new Integer(((Long) index).intValue()); + } else if (index instanceof String) { + try { + idx = new Integer((String) index); + } catch (NumberFormatException e) { + throw new InvalidTypeException( + String + .format( + "variable '%s' is an array indexed by '%s', however the index cannot be converted to an integer", + this.name, index)); + } + } else { + throw new InvalidTypeException( + String + .format( + "variable '%s' is an array indexed by '%s', however the index must be an integer or string not %s", + this.name, index, index.getClass().getSimpleName())); + } + + try { + value = list.get(idx); + } catch (IndexOutOfBoundsException e) { + throw new UndefinedValueException( + String + .format( + "variable '%s' is an array of size %d indexed by '%s', however the index is out of bounds", + this.name, list.size(), idx)); + } + } else if (base instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) base; + String idx = null; + if (index instanceof String) { + idx = (String) index; + } else { + throw new InvalidTypeException(String.format( + "variable '%s' is a map indexed by '%s', however the index must be a string not %s", + this.name, index, index.getClass().getSimpleName())); + } + if (!map.containsKey(idx)) { + throw new UndefinedValueException(String.format( + "variable '%s' is a map indexed by '%s', however the index does not exist", + this.name, index)); + } + value = map.get(idx); + } else { + throw new InvalidTypeException(String.format( + "variable '%s' is indexed by '%s', variable must be an array or map, not %s", + this.name, index, base.getClass().getSimpleName())); + + } + } + this.type = classify(value); + return value; + } + + void set(Object value) { + set(value, null); + } + + void set(Object value, Object index) { + + if (this.storageType == TokenStorageType.CONSTANT) { + throw new InvalidTypeException("cannot assign to a constant"); + } + + if (index == null) { + index = this.index; + } + + if (index == null) { // scalar types + this.namespace.put(this.name, value); + } else { + Object base = null; + + if (this.namespace.containsKey(this.name)) { + base = this.namespace.get(this.name); + } else { + throw new UndefinedValueException(String.format("variable '%s' not defined", this.name)); + } + + if (base instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) base; + Integer idx = null; + + if (index instanceof Long) { + idx = new Integer(((Long) index).intValue()); + } else if (index instanceof String) { + try { + idx = new Integer((String) index); + } catch (NumberFormatException e) { + throw new InvalidTypeException( + String + .format( + "variable '%s' is an array indexed by '%s', however the index cannot be converted to an integer", + this.name, index)); + } + } else { + throw new InvalidTypeException( + String + .format( + "variable '%s' is an array indexed by '%s', however the index must be an integer or string not %s", + this.name, index, index.getClass().getSimpleName())); + } + + try { + value = list.set(idx, value); + } catch (IndexOutOfBoundsException e) { + throw new UndefinedValueException( + String + .format( + "variable '%s' is an array of size %d indexed by '%s', however the index is out of bounds", + this.name, list.size(), idx)); + } + } else if (base instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) base; + String idx = null; + if (index instanceof String) { + idx = (String) index; + } else { + throw new InvalidTypeException(String.format( + "variable '%s' is a map indexed by '%s', however the index must be a string not %s", + this.name, index, index.getClass().getSimpleName())); + } + if (!map.containsKey(idx)) { + throw new UndefinedValueException(String.format( + "variable '%s' is a map indexed by '%s', however the index does not exist", + this.name, index)); + } + value = map.put(idx, value); + } else { + throw new InvalidTypeException(String.format( + "variable '%s' is indexed by '%s', variable must be an array or map, not %s", + this.name, index, base.getClass().getSimpleName())); + + } + } + } + + public Object load() { + this.value = get(); + return this.value; + } + + public Object load(Object index) { + this.value = get(index); + return this.value; + } + + public String getStringValue() { + if (this.type == TokenType.STRING) { + return (String) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.STRING, this.type)); + } + } + + public List getListValue() { + if (this.type == TokenType.ARRAY) { + @SuppressWarnings("unchecked") + List list = (List) this.value; + return list; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.ARRAY, this.type)); + } + } + + public Map getMapValue() { + if (this.type == TokenType.MAP) { + @SuppressWarnings("unchecked") + Map map = (Map) this.value; + return map; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.MAP, this.type)); + } + } + + public Long getLongValue() { + if (this.type == TokenType.INTEGER) { + return (Long) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.INTEGER, this.type)); + } + } + + public Boolean getBooleanValue() { + if (this.type == TokenType.BOOLEAN) { + return (Boolean) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.BOOLEAN, this.type)); + } + } + + public Double getDoubleValue() { + if (this.type == TokenType.REAL) { + return (Double) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.REAL, this.type)); + } + } + + public Object getNullValue() { + if (this.type == TokenType.NULL) { + return this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.NULL, this.type)); + } + } + + public Object getObjectValue() { + return this.value; + } + + + +} diff --git a/java/src/main/java/com/redhat/IdPMapping/UndefinedValueException.java b/java/src/main/java/com/redhat/IdPMapping/UndefinedValueException.java new file mode 100644 index 0000000..d3e429c --- /dev/null +++ b/java/src/main/java/com/redhat/IdPMapping/UndefinedValueException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 Red Hat + * All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.idpmapping; + +/** + * Exception thrown when a statement references an undefined value. + * + * @author John Dennis + */ + +public class UndefinedValueException extends RuntimeException { + + private static final long serialVersionUID = -1607453931670834435L; + + public UndefinedValueException() {} + + public UndefinedValueException(String message) { + super(message); + } + + public UndefinedValueException(Throwable cause) { + super(cause); + } + + public UndefinedValueException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/src/main/java/com/redhat/app/MappingApp.java b/java/src/main/java/com/redhat/app/MappingApp.java new file mode 100644 index 0000000..511a823 --- /dev/null +++ b/java/src/main/java/com/redhat/app/MappingApp.java @@ -0,0 +1,57 @@ +package com.redhat.app; + + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; + +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.Logger; + +import com.redhat.IdpMapping.InvalidTypeException; +import com.redhat.IdpMapping.IdpJson; +import com.redhat.IdpMapping.RuleProcessor; +import com.redhat.IdpMapping.InvalidRuleException; +import com.redhat.IdpMapping.UndefinedValueException; + +class MappingApp { + private static final Logger log = Logger.getLogger(MappingApp.class); + + + private static String loadFile(String path) throws IOException { + byte[] encoded = Files.readAllBytes(Paths.get(path)); + return new String(encoded, "UTF-8"); + } + + public static void main(String[] args) { + BasicConfigurator.configure(); + log.info("Federated Mapping"); + + String rulesFilename = args[0]; + String assertionFilename = "/home/jdennis/src/misc/federation_mapping/assertion-01.json"; + String assertionJson; + Map mapped; + IdpJson json = new IdpJson(); + RuleProcessor rp = null; + + try { + rp = new RuleProcessor(Paths.get(rulesFilename), null); // FIXME, mappings parameter + assertionJson = loadFile(assertionFilename); + + } catch (IOException e) { + log.error(e); + return; + } + try { + mapped = rp.process(assertionJson); + System.out.println(String.format("mapped=%s", mapped)); + if (mapped != null) { + System.out.println(json.dumpJson(mapped)); + } + } catch (InvalidRuleException | UndefinedValueException | InvalidTypeException e) { + // e.printStackTrace(); + log.error(e); + } + } +} diff --git a/python/idp_mapping.py b/python/idp_mapping.py new file mode 100755 index 0000000..14971d5 --- /dev/null +++ b/python/idp_mapping.py @@ -0,0 +1,1055 @@ +#!/usr/bin/python + + +import copy +import json +import logging +import re +import six + +def _(string): + return string + +class InvalidRuleError(ValueError): + pass + +class UndefinedValueError(ValueError): + pass + +class StatementError(ValueError): + pass + +class IllegalStateError(RuntimeError): + pass + +RULE_FAIL = 0 +RULE_SUCCESS = 1 +BLOCK_CONTINUE = 2 +STATEMENT_CONTINUE = 3 + +rule_result_names = { + RULE_FAIL: 'RULE_FAIL', + RULE_SUCCESS: 'RULE_SUCCESS', + BLOCK_CONTINUE: 'BLOCK_CONTINUE', + STATEMENT_CONTINUE: 'STATEMENT_CONTINUE', +} + +def rule_result_name(result): + return rule_result_names.get(result, "unknown") + +# +# Reserved variables +# +ASSERTION = 'assertion' +RULE_NUMBER = 'rule_number' +RULE_NAME = 'rule_name' +BLOCK_NUMBER = 'block_number' +BLOCK_NAME = 'block_name' +STATEMENT_NUMBER = 'statement_number' +REGEXP_ARRAY_VARIABLE = 'regexp_array' +REGEXP_MAP_VARIABLE = 'regexp_map' + +class Token(object): + # Regexp to identify a variable beginning with $ + # Supports array notation, e.g. $foo[bar] + # Optional delimiting braces may be used to separate variable from + # surrounding text. + # + # Examples: $foo ${foo} $foo[bar] ${foo[bar]} + # where foo is the variable name and bar is the array index. + # + # Identifer is any alphabetic followed by alphanumeric or underscore + VARIABLE_PAT = (r'(?' + self.statement_id_format = ('') + + if isinstance(rules, basestring): + rules = json.loads(rules) + self.rules = rules + if mappings is None: + self.mappings = {} + else: + self.mappings = mappings + + + def rule_id(self, namespace): + return self.substitute_variables(self.rule_id_format, namespace) + + def statement_id(self, namespace): + return self.substitute_variables(self.statement_id_format, namespace) + + # FIXME, not used + def to_string(self, value): + Token.classify(value) # raises TypeError if not supported type + return json.dumps(value) + + def substitute_variables(self, string, namespace): + def get_replacement(match): + token = Token(match.group(0), namespace) + token.load() + if token.type == Token.TYPE_STRING: + replacement = token.value + else: + replacement = six.text_type(token.value) + return replacement + + return Token.VARIABLE_RE.sub(get_replacement, string) + + # FIXME, should we be passing namespace? Just used for rule_id + def get_mapping(self, namespace, rule): + mapping = rule.get('mapping') + if mapping is not None: + self.log.debug("using mapping local to rule %s mapping=%s", + self.rule_id(namespace), mapping) + return mapping + + mapping_name = rule.get('mapping_name') + if mapping_name is None: + raise InvalidRuleError("%s rule does not define mapping nor mapping_name unable to load mapping" % + (self.rule_id(namespace))) + mapping = self.mappings.get(mapping_name) + if mapping is None: + raise InvalidRuleError("%s rule specifies mapping_name '%s' but a mapping by that name does not exist, unable to load mapping" % + (self.rule_id(namespace))) + self.log.debug("using named mapping '%s' from rule %s mapping=%s", + mapping_name, self.rule_id(namespace), mapping) + return mapping + + def get_verb(self, statement): + if len(statement) < 1: + raise InvalidRuleError("statement has no verb") + try: + verb = Token(statement[0], None) + except Exception as exc: + raise InvalidRuleError("statement first member (i.e. verb) error %s" % + (exc)) + + if verb.type != Token.TYPE_STRING: + raise InvalidRuleError("statement first member (i.e. verb) must be a string, not %s" % + (verb.type_name)) + return verb.value.lower() + + def get_token(self, verb, statement, index, namespace, + storage_type=None, token_types=None): + try: + item = statement[index] + except IndexError: + raise InvalidRuleError("verb '%s' requires at least %d items but only %d are available." % + (verb, index+1, len(statement))) + + try: + token = Token(item, namespace) + except Exception as exc: + raise StatementError("parameter %d, %s" % (index, exc)) + + + if storage_type is not None: + if token.storage_type not in storage_type: + raise TypeError("verb '%s' requires parameter #%d to have storage types %s not %s. statement=%s" % + (verb, index, + [Token.get_storage_type_name(x) + for x in storage_type], + token.storage_type_name, statement)) + + if token_types is not None: + try: + token.load() # Note, Token.load() sets the Token.type + except UndefinedValueError: + # OK if not yet defined + pass + + if token.type not in token_types: + raise TypeError("verb '%s' requires parameter #%d to have types %s, not %s. statement=%s" % + (verb, index, + [Token.get_type_name(x) for x in sorted(token_types)], + token.type_name, statement)) + + return token + + def get_parameter(self, verb, statement, index, namespace, token_types=None): + try: + item = statement[index] + except IndexError: + raise InvalidRuleError("verb '%s' requires at least %d items but only %d are available." % + (verb, index+1, len(statement))) + + try: + token = Token(item, namespace) + except Exception as exc: + raise StatementError("parameter %d, %s" % (index, exc)) + + + if token_types is not None: + token.get() # Note, Token.get() sets the Token.type + + if token.type not in token_types: + raise TypeError("verb '%s' requires parameter #%d to have types %s, not %s. statement=%s" % + (verb, index, + [Token.get_type_name(x) for x in sorted(token_types)], + token.type_name, statement)) + + token.load() + return token + + def get_raw_parameter(self, verb, statement, index, token_types=None): + try: + item = statement[index] + except IndexError: + raise InvalidRuleError("verb '%s' requires at least %d items but only %d are available." % + (verb, index+1, len(statement))) + + if token_types is not None: + item_type = Token.classify(item) + + if item_type not in token_types: + raise TypeError("verb '%s' requires parameter #%d to have types %s, not %s. statement=%s" % + (verb, index, + [Token.get_type_name(x) for x in sorted(token_types)], + Token.get_type_name(item_type), statement)) + + return item + + def get_variable(self, verb, statement, index, namespace): + + try: + item = statement[index] + except IndexError: + raise InvalidRuleError("verb '%s' requires at least %d items but only %d are available." % + (verb, index+1, len(statement))) + + try: + token = Token(item, namespace) + except Exception as exc: + raise StatementError("parameter %d, %s" % (index, exc)) + + if token.storage_type != Token.STORAGE_TYPE_VARIABLE: + raise TypeError("verb '%s' requires parameter #%d to be a variable not %s. statement=%s" % + (verb, index, token.storage_type_name, statement)) + + return token + + + + def process(self, assertion): + self.success = True + for rule_number, rule in enumerate(self.rules): + namespace = {} + namespace[RULE_NUMBER] = rule_number + namespace[RULE_NAME] = '' + namespace[ASSERTION] = copy.deepcopy(assertion) + try: + result = self.process_rule(namespace, rule) + except Exception as exc: + self.log.error("%s", exc) # FIXME log.exception? + raise + if result == RULE_SUCCESS: + mapped = {} + mapping = self.get_mapping(namespace, rule) + for k, v in mapping.iteritems(): + try: + token = Token(v, namespace) + new_value = token.get() + except Exception as e: + raise InvalidRule("%s unable to get value for mapping %s=%s, %s" % (self.rule_id(namespace), k, v, e)) + mapped[k] = new_value + return mapped + return None + + + def process_rule(self, namespace, rule): + statement_blocks = rule.get('statement_blocks') + if statement_blocks is None: + raise InvalidRuleError("rule missing 'statement_blocks'") + + result = BLOCK_CONTINUE + for block_number, block in enumerate(statement_blocks): + namespace[BLOCK_NUMBER] = block_number + namespace[BLOCK_NAME] = '' + result = self.process_block(namespace, block) + if result in (RULE_SUCCESS, RULE_FAIL): + break + elif result == BLOCK_CONTINUE: + continue + else: + raise ValueError("%s unexpected block result: %s" % + (self.statement_id(namespace), result)) + if result in (RULE_SUCCESS, BLOCK_CONTINUE): + return RULE_SUCCESS + else: + return RULE_FAIL + + def process_block(self, namespace, statements): + result = STATEMENT_CONTINUE + + for statement_number, statement in enumerate(statements): + namespace[STATEMENT_NUMBER] = statement_number + try: + result = self.process_statement(namespace, statement) + except Exception as exc: + raise StatementError("%s statement=%s %s" % (self.statement_id(namespace), statement, exc)) + if result in (BLOCK_CONTINUE, RULE_SUCCESS, RULE_FAIL): + break + elif result == STATEMENT_CONTINUE: + continue + else: + raise ValueError("%s unexpected statement result: %s" % + (self.statement_id(namespace), result)) + + if result == STATEMENT_CONTINUE: + result = BLOCK_CONTINUE + + return result + + def process_statement(self, namespace, statement): + result = STATEMENT_CONTINUE + + verb = self.get_verb(statement) + + if verb == 'set': + result = self.verb_set(verb, namespace, statement) + elif verb == 'length': + result = self.verb_length(verb, namespace, statement) + elif verb == 'interpolate': + result = self.verb_interpolate(verb, namespace, statement) + elif verb == 'append': + result = self.verb_append(verb, namespace, statement) + elif verb == 'unique': + result = self.verb_unique(verb, namespace, statement) + elif verb == 'split': + result = self.verb_split(verb, namespace, statement) + elif verb == 'join': + result = self.verb_join(verb, namespace, statement) + elif verb == 'lower': + result = self.verb_lower(verb, namespace, statement) + elif verb == 'upper': + result = self.verb_upper(verb, namespace, statement) + elif verb == 'in': + result = self.verb_in(verb, namespace, statement) + elif verb == 'not_in': + result = self.verb_not_in(verb, namespace, statement) + elif verb == 'compare': + result = self.verb_compare(verb, namespace, statement) + elif verb == 'regexp': + result = self.verb_regexp(verb, namespace, statement) + elif verb == 'regexp_replace': + result = self.verb_regexp_replace(verb, namespace, statement) + elif verb == 'exit': + result = self.verb_exit(verb, namespace, statement) + elif verb == 'continue': + result = self.verb_continue(verb, namespace, statement) + else: + raise InvalidRuleError("unknown verb '%s'" % (verb)) + + return result + + def verb_set(self, verb, namespace, statement): + variable = self.get_variable(verb, statement, 1, namespace) + parameter = self.get_parameter(verb, statement, 2, namespace) + + variable.set(parameter.value) + self.success = True + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s variable: %s=%s", + self.statement_id(namespace), verb, self.success, + variable, variable.get()) + + return STATEMENT_CONTINUE + + def verb_length(self, verb, namespace, statement): + variable = self.get_variable(verb, statement, 1, namespace) + parameter = self.get_parameter(verb, statement, 2, namespace, + set([Token.TYPE_ARRAY, + Token.TYPE_MAP, + Token.TYPE_STRING])) + + try: + length = len(parameter.value) + except Exception as exc: + raise ValueError("verb '%s' failed, variable='%s' parameter='%s': %s" % + (verb, variable, parameter.value, exc)) + variable.set(length) + self.success = True + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s variable: %s=%s parameter=%s", + self.statement_id(namespace), verb, self.success, + variable, variable.get(), parameter.value) + + return STATEMENT_CONTINUE + + def verb_interpolate(self, verb, namespace, statement): + variable = self.get_variable(verb, statement, 1, namespace) + string = self.get_raw_parameter(verb, statement, 2, + set([Token.TYPE_STRING])) + + try: + new_value = self.substitute_variables(string, namespace) + except Exception as exc: + raise ValueError("verb '%s' failed, variable='%s' string='%s': %s" % + (verb, variable, string, exc)) + variable.set(new_value) + self.success = True + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s variable: %s=%s string='%s'", + self.statement_id(namespace), verb, self.success, + variable, variable.get(), string) + + return STATEMENT_CONTINUE + + def verb_append(self, verb, namespace, statement): + variable = self.get_token(verb, statement, 1, namespace, + set([Token.STORAGE_TYPE_VARIABLE]), + set([Token.TYPE_ARRAY])) + item = self.get_parameter(verb, statement, 2, namespace) + + try: + variable.get().append(item.value) + except Exception as exc: + raise ValueError("verb '%s' failed, variable='%s' item='%s': %s" % + (verb, variable, item.value, exc)) + self.success = True + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s variable: %s=%s item=%s", + self.statement_id(namespace), verb, self.success, + variable, variable.get(), item.value) + + return STATEMENT_CONTINUE + + def verb_unique(self, verb, namespace, statement): + variable = self.get_variable(verb, statement, 1, namespace) + array = self.get_parameter(verb, statement, 2, namespace, + set([Token.TYPE_ARRAY])) + + seen = set() + new_value = [] + + for member in array.value: + if member in seen: + continue + new_value.append(member) + seen.add(member) + + + variable.set(new_value) + self.success = True + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s variable: %s=%s array=%s", + self.statement_id(namespace), verb, self.success, + variable, variable.get(), array.value) + + return STATEMENT_CONTINUE + + def verb_split(self, verb, namespace, statement): + variable = self.get_variable(verb, statement, 1, namespace) + string = self.get_parameter(verb, statement, 2, namespace, + set([Token.TYPE_STRING])) + pattern = self.get_parameter(verb, statement, 3, namespace, + set([Token.TYPE_STRING])) + + try: + new_value = re.split(pattern.value, string.value) + except Exception as exc: + raise ValueError("verb '%s' failed, pattern='%s' string='%s': %s" % + (verb, pattern.value, string.value, exc)) + + variable.set(new_value) + self.success = True + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s variable: %s=%s string='%s' pattern='%s'", + self.statement_id(namespace), verb, self.success, + variable, variable.get(), string.value, pattern.value) + + return STATEMENT_CONTINUE + + def verb_join(self, verb, namespace, statement): + variable = self.get_variable(verb, statement, 1, namespace) + array = self.get_parameter(verb, statement, 2, namespace, + set([Token.TYPE_ARRAY])) + conjunction = self.get_parameter(verb, statement, 3, namespace, + set([Token.TYPE_STRING])) + + try: + new_value = conjunction.value.join(array.value) + except Exception as exc: + raise ValueError("verb '%s' failed, array=%s conjunction='%s'': %s" % + (verb, array.value, conjunction.value, exc)) + + + variable.set(new_value) + self.success = True + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s variable: %s=%s array=%s conjunction='%s'", + self.statement_id(namespace), verb, self.success, + variable, variable.get(), array.value, conjunction.value) + + return STATEMENT_CONTINUE + + def verb_lower(self, verb, namespace, statement): + variable = self.get_variable(verb, statement, 1, namespace) + parameter = self.get_parameter(verb, statement, 2, namespace, + token_types=set([Token.TYPE_STRING, + Token.TYPE_ARRAY, + Token.TYPE_MAP])) + + try: + if parameter.type == Token.TYPE_STRING: + new_value = parameter.value.lower() + elif parameter.type == Token.TYPE_ARRAY: + new_value = [x.lower() for x in parameter.value] + elif parameter.type == Token.TYPE_MAP: + new_value = dict((k.lower(), v) + for k, v in parameter.value.iteritems()) + else: + raise IllegalStateError("unexpected token type: %s" % + (parameter.type_name)) + except Exception as exc: + raise ValueError("verb '%s' failed, variable='%s' parameter='%s': %s" % + (verb, variable, parameter.value, exc)) + variable.set(new_value) + self.success = True + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s variable: %s=%s parameter=%s", + self.statement_id(namespace), verb, self.success, + variable, variable.get(), parameter) + + return STATEMENT_CONTINUE + + def verb_upper(self, verb, namespace, statement): + variable = self.get_variable(verb, statement, 1, namespace) + parameter = self.get_parameter(verb, statement, 2, namespace, + token_types=set([Token.TYPE_STRING, + Token.TYPE_ARRAY, + Token.TYPE_MAP])) + + try: + if parameter.type == Token.TYPE_STRING: + new_value = parameter.value.upper() + elif parameter.type == Token.TYPE_ARRAY: + new_value = [x.upper() for x in parameter.value] + elif parameter.type == Token.TYPE_MAP: + new_value = dict((k.upper(), v) + for k, v in parameter.value.iteritems()) + else: + raise IllegalStateError("unexpected token type: %s" % + (parameter.type_name)) + except Exception as exc: + raise ValueError("verb '%s' failed, variable='%s' parameter='%s': %s" % + (verb, variable, parameter.value, exc)) + variable.set(new_value) + self.success = True + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s variable: %s=%s parameter=%s", + self.statement_id(namespace), verb, self.success, + variable, variable.get(), parameter) + + return STATEMENT_CONTINUE + + def verb_in(self, verb, namespace, statement): + member = self.get_parameter(verb, statement, 1, namespace) + collection = self.get_parameter(verb, statement, 2, namespace, + token_types=set([Token.TYPE_ARRAY, + Token.TYPE_MAP, + Token.TYPE_STRING])) + + try: + self.success = member.value in collection.value + except Exception as exc: + raise ValueError("verb '%s' failed, member='%s' collection='%s': %s" % + (verb, member.value, collection.value, exc)) + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s member=%s collection=%s", + self.statement_id(namespace), verb, self.success, + member.value, collection.value) + + return STATEMENT_CONTINUE + + def verb_not_in(self, verb, namespace, statement): + member = self.get_parameter(verb, statement, 1, namespace) + collection = self.get_parameter(verb, statement, 2, namespace, + token_types=set([Token.TYPE_ARRAY, + Token.TYPE_MAP, + Token.TYPE_STRING])) + + try: + self.success = member.value not in collection.value + except Exception as exc: + raise ValueError("verb '%s' failed, member='%s' collection='%s': %s" % + (verb, member.value, collection.value, exc)) + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s member=%s collection=%s", + self.statement_id(namespace), verb, self.success, + member.value, collection.value) + + return STATEMENT_CONTINUE + + def verb_compare(self, verb, namespace, statement): + left = self.get_parameter(verb, statement, 1, namespace) + op = self.get_parameter(verb, statement, 2, namespace, + set([Token.TYPE_STRING])) + right = self.get_parameter(verb, statement, 3, namespace) + + if left.type != right.type: + raise TypeError("verb '%s' both items must have the same type left is %s and right is %s" % + (verb, + left.type_name, right.type_name)) + + try: + if op.value == '==': + self.success = left.value == right.value + elif op.value == '!=': + self.success = left.value != right.value + elif op.value == '<': + self.success = left.value < right.value + elif op.value == '<=': + self.success = left.value <= right.value + elif op.value == '>': + self.success = left.value > right.value + elif op.value == '>=': + self.success = left.value >= right.value + else: + raise InvalidRuleError("verb '%s' has unknown comparison operator '%s'" % + (verb, op.value)) + except Exception as exc: + self.success = False + raise ValueError("verb '%s' failed, left=%s op='%s' right=%s, %s" % + (verb, left.value, op.value, right.value, exc)) + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s left=%s op='%s' right=%s", + self.statement_id(namespace), verb, self.success, + left.value, op.value, right.value) + + return STATEMENT_CONTINUE + + def verb_regexp(self, verb, namespace, statement): + string = self.get_parameter(verb, statement, 1, namespace, + set([Token.TYPE_STRING])) + pattern = self.get_parameter(verb, statement, 2, namespace, + set([Token.TYPE_STRING])) + + try: + match = re.search(pattern.value, string.value) + except Exception as exc: + self.success = False + raise ValueError("verb '%s' failed, string='%s' pattern='%s', %s" % + (verb, string.value, pattern.value, exc)) + if match: + self.success = True + + # Note, match.groups() returns a tuple + # containing all the subgroups of the match, + # from 1 up to however many groups are in the + # pattern. But we want to allow zero-based + # indexing as well as access to group 0, + # therefore we insert group 0 at the head of + # the list. + + result = list(match.groups()) + result.insert(0, match.group(0)) + namespace[REGEXP_ARRAY_VARIABLE] = result + namespace[REGEXP_MAP_VARIABLE] = match.groupdict() + else: + self.success = False + namespace[REGEXP_ARRAY_VARIABLE] = [] + namespace[REGEXP_MAP_VARIABLE] = {} + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s string='%s' pattern='%s' %s=%s %s=%s", + self.statement_id(namespace), verb, self.success, + string.value, pattern.value, + REGEXP_ARRAY_VARIABLE, + namespace[REGEXP_ARRAY_VARIABLE], + REGEXP_MAP_VARIABLE, + namespace[REGEXP_MAP_VARIABLE]) + + return STATEMENT_CONTINUE + + def verb_regexp_replace(self, verb, namespace, statement): + variable = self.get_variable(verb, statement, 1, namespace,) + string = self.get_parameter(verb, statement, 2, namespace, + set([Token.TYPE_STRING])) + pattern = self.get_parameter(verb, statement, 3, namespace, + set([Token.TYPE_STRING])) + replacement = self.get_parameter(verb, statement, 4, namespace, + set([Token.TYPE_STRING])) + + try: + new_value = re.sub(pattern.value, replacement.value, string.value) + except Exception as exc: + self.success = False + raise ValueError("verb '%s' verb failed, pattern='%s' replacement='%s', %s" % + (verb, pattern.value, replacement.value, exc)) + else: + variable.set(new_value) + self.success = True + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s variable: %s=%s string='%s' pattern='%s' replacement='%s'", + self.statement_id(namespace), verb, self.success, + variable, variable.get(), + string.value, pattern.value, replacement.value) + + return STATEMENT_CONTINUE + + def verb_exit(self, verb, namespace, statement): + statement_result = STATEMENT_CONTINUE + + exit_status_param = self.get_parameter(verb, statement, 1, namespace, + set([Token.TYPE_STRING])) + criteria_param = self.get_parameter(verb, statement, 2, namespace, + set([Token.TYPE_STRING])) + + exit_status = exit_status_param.value.lower() + criteria = criteria_param.value.lower() + + if exit_status == 'rule_succeeds': + result = RULE_SUCCESS + elif exit_status == 'rule_fails': + result = RULE_FAIL + else: + raise InvalidRuleError("verb='%s' unknown exit status '%s'" % + (verb, exit_status)) + + + if criteria == 'if_success': + if self.success: + do_exit = True + else: + do_exit = False + elif criteria == 'if_not_success': + if not self.success: + do_exit = True + else: + do_exit = False + elif criteria == 'always': + do_exit = True + elif criteria == 'never': + do_exit = False + else: + raise InvalidRuleError("verb='%s' unknown exit criteria '%s'" % + (verb, criteria)) + + if do_exit: + statement_result = result + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s status=%s criteria=%s exiting=%s result=%s", + self.statement_id(namespace), verb, self.success, + exit_status, criteria, do_exit, + rule_result_name(statement_result)) + + return statement_result + + + def verb_continue(self, verb, namespace, statement): + statement_result = STATEMENT_CONTINUE + + criteria_param = self.get_parameter(verb, statement, 1, namespace, + set([Token.TYPE_STRING])) + criteria = criteria_param.value.lower() + + if criteria == 'if_success': + if self.success: + do_continue = True + else: + do_continue = False + elif criteria == 'if_not_success': + if not self.success: + do_continue = True + else: + do_continue = False + elif criteria == 'always': + do_continue = True + elif criteria == 'never': + do_continue = False + else: + raise InvalidRuleError("verb='%s' unknown continue criteria '%s'" % + (verb, criteria)) + + if do_continue: + statement_result = BLOCK_CONTINUE + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("%s verb='%s' success=%s criteria=%s continuing=%s result=%s", + self.statement_id(namespace), verb, self.success, + criteria, do_continue, + rule_result_name(statement_result)) + + return statement_result + diff --git a/python/mapping_app.py b/python/mapping_app.py new file mode 100755 index 0000000..c590061 --- /dev/null +++ b/python/mapping_app.py @@ -0,0 +1,55 @@ +#!/usr/bin/python + +import json +import logging +from idp_mapping import RuleProcessor +import sys +import traceback + +LOG = logging.getLogger() +logging.basicConfig(level=logging.INFO, format='%(message)s') + +rule_filename = '/home/jdennis/src/misc/federation_mapping/rules-python-01.json' +assertion_filename = '/home/jdennis/src/misc/federation_mapping/assertion-01.json' + +def assertion_from_file(filename): + with open(filename) as stream: + assertion = json.load(stream) + return assertion + +def assertion_from_string(string): + assertion = json.loads(string) + return assertion + +def main(): + if True: + rule_processor = RuleProcessor.from_file(rule_filename) + + if False: + with open(rule_filename) as stream: + rule_processor = RuleProcessor.from_stream(stream) + + if False: + with open(rule_filename) as stream: + string = stream.read() + rule_processor = RuleProcessor.from_string(string) + + assertion = assertion_from_file(assertion_filename) + + try: + mapped = rule_processor.process(assertion) + if mapped is None: + print "no rules matched" + else: + for k, v in mapped.iteritems(): + print "%s: %s" % (k, v) + mapped_json = json.dumps(mapped, indent=4) + print "\nmapped JSON" + print mapped_json + except Exception as exc: + print "FAIL: %s" % exc + traceback.print_exc() + sys.exit(1) + sys.exit(0) + +main() -- cgit